Spring: Реализация TaskExecutor c поддержкой транзакций

в 11:01, , рубрики: design patterns, java, java concurrency, spring framework

Spring, позаботившись о разработчиках, предлагает удобный и простой фасад для взаимодействия с менеджером транзакций. Однако всегда ли стандартного механизма будет достаточно для реализации изощрённых архитектурных идей? Очевидно — нет.

В этом посте пойдёт речь о возможностях Spring —

  • взглянем на примеры стандартного управления транзакциями с помощью аннотаций,
  • поймём — когда решить задачу с помощью аннотаций не получится,
  • и, судя по заголовку статьи, дадим пример реализации транзакционного исполнения кода в новом потоке, создавемых с помощью Spring TaskExecutor.


Наиболее общеупотребимо задание транзакций с помощью аннотации @Transactional. Что бы воспользоваться этим механизмом достаточно сконфигурировать предпочитаемый TransactionManager и включить обработку аннотаций. В случае конфигурации с помощью XML файлов это выглядит примерно так:

    .....
     <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="dataSource" ref="dataSource" />
        <property name="entityManagerFactory" ref="entityManagerFactory" />
        <property name="jpaDialect" ref="jpaDialect"/>
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager"/>
    ....

Использовать далее это так же просто как «навесить» аннотацию @Transactional на реализацию метода интерфейса или над всем классом-реализацией.

@Service
public class FooServiceImpl implements FooService {
    @Autowired
    private FooDao fooDao;

    @Transactional
    @Override
    public void update(Foo entity)  {
        fooDao.merge(entity);
    }
}

Однако, что бы эффективно использовать этот механизм нужно помнить о нескольких, невсегда очевидных тонкостях:

  • Класс должен быть объявлен как bean (c помощью аннотаций, code-based или в xml конфигурации контейнера)
  • Класс должен реализовывать интерфейс, иначе контейнеру будет сложно создать прокси-объект, с помощью которого и выполняется управление транзакцией.
  • Вызов транзакционных методов из другого метода того же класса не приведёт к созданию транзакции! (следствие из предыдущего пункта)

Подобный механизм позволяет не писать код управления транзакцией каждый раз!

Однако, могут ли быть ситуации, когда этого механизма будет недостаточно? или возможен ли контекст, в котором аннотациями обойтись не удастся? Боюсь, ответ очевиден. К примеру, мы можем захотеть выполнить часть кода в одной транзакции, а другую часть – во второй (тут скорее архитектурно верным будет разделить метод на два). У читателей, думаю, есть и свои примеры.

Более реалистичен пример, когда часть кода нужно выполнить асинхронно:

@Service
public class FooServiceImpl implements FooService {
    @Autowired
    private TaskExecutor taskExecutor;
    @Autowired
    private FooDao fooDao;

    @Transactional
    @Override
    public void update(Foo entity)  {
        fooDao.merge(entity);
        taskExecutor.run(new Runnable() {
            public void run() {
                someLongTimeOperation(entity);
            }
        });
    }
    
    @Transactional
    @Override
    public void someLongTimeOperation(Foo entity)  {
        // тут набор ресурсоёмких операций
    }
}

Что же получается: до старта метода update() создаётся транзакция, затем выполняются операции из тела, а по выходу из метода транзакция закрывается. Но в нашем случае создаётся новый поток, в котором будет исполнен код. И весьма очевидно, что на момент выхода из метода update() и сопутствующего уничтожения транзакции, выполнение кода во втором запущенном потоке может/будет продолжаться. Как итог, по завершению метода, во втором потоке получим исключение и вся транзакция «ролбэкнится».

К предудщему примеру добавим ручное создание транзакции:

@Service
public class FooServiceImpl implements FooService {
    @Autowired
    private TaskExecutor taskExecutor;
    @Autowired
    private FooDao fooDao;

    @Transactional
    @Override
    public void update(final Foo entity)  {
       fooDao.merge(entity);
       final TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
       taskExecutor.execute(new Runnable() {
            @Override
            public void run() {
                transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                    @Override
                    protected void doInTransactionWithoutResult(TransactionStatus status) {
                        someLongTimeOperation(entity);
                    }
                });
            }
        });     
    }

    @Transactional
    @Override
    public void someLongTimeOperation(Foo entity)  {
        // тут набор ресурсоёмких операций
    }
}

Теперь someLongTimeOperation() исполняется асинхронно и в выделенной транзакции. Однако, хочется обощённой реализации, что бы не дублировать громоздкий код ручного управления.

Что ж… вот и она:

public interface TransactionalAsyncTaskExecutor extends AsyncTaskExecutor {
    void execute(Runnable task, Integer propagation, Integer isolationLevel);
}

public class DelegatedTransactionalAsyncTaskExecutor implements InitializingBean, TransactionalAsyncTaskExecutor {
    private PlatformTransactionManager transactionManager;
    private AsyncTaskExecutor delegate;
    private TransactionTemplate sharedTransactionTemplate;

    public DelegatedTransactionalAsyncTaskExecutor() {
    }

    public DelegatedTransactionalAsyncTaskExecutor(PlatformTransactionManager transactionManager, AsyncTaskExecutor delegate) {
        this.transactionManager = transactionManager;
        this.delegate = delegate;
    }

    @Override
    public void execute(final Runnable task, Integer propagation, Integer isolationLevel) {
        final TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        transactionTemplate.setPropagationBehavior(propagation);
        transactionTemplate.setIsolationLevel(isolationLevel);
        delegate.execute(new Runnable() {
            @Override
            public void run() {
                transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                    @Override
                    protected void doInTransactionWithoutResult(TransactionStatus status) {
                        task.run();
                    }
                });
            }
        });
    }

    @Override
    public void execute(final Runnable task) {
        execute(task, TransactionDefinition.PROPAGATION_REQUIRED, TransactionDefinition.ISOLATION_DEFAULT);
    }

    @Override
    public void execute(final Runnable task, long startTimeout) {
        final TransactionTemplate transactionTemplate = getSharedTransactionTemplate();
        delegate.execute(new Runnable() {
            @Override
            public void run() {
                transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                    @Override
                    protected void doInTransactionWithoutResult(TransactionStatus status) {
                        task.run();
                    }
                });
            }
        }, startTimeout);

    }

    @Override
    public Future<?> submit(final Runnable task) {
        final TransactionTemplate transactionTemplate = getSharedTransactionTemplate();
        return delegate.submit(new Runnable() {
            @Override
            public void run() {
                transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                    @Override
                    protected void doInTransactionWithoutResult(TransactionStatus status) {
                        task.run();
                    }
                });
            }
        });
    }

    @Override
    public <T> Future<T> submit(final Callable<T> task) {
        final TransactionTemplate transactionTemplate = getSharedTransactionTemplate();
        return delegate.submit(new Callable<T>() {
            @Override
            public T call() throws Exception {
                return transactionTemplate.execute(new TransactionCallback<T>() {
                    @Override
                    public T doInTransaction(TransactionStatus status) {
                        T result = null;
                        try {
                            result = task.call();
                        } catch (Exception e) {
                            e.printStackTrace();
                            status.setRollbackOnly();
                        }
                        return result;
                    }
                });
            }
        });
    }

    public PlatformTransactionManager getTransactionManager() {
        return transactionManager;
    }

    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public AsyncTaskExecutor getDelegate() {
        return delegate;
    }

    public void setDelegate(AsyncTaskExecutor delegate) {
        this.delegate = delegate;
    }

    public TransactionTemplate getSharedTransactionTemplate() {
        return sharedTransactionTemplate;
    }

    public void setSharedTransactionTemplate(TransactionTemplate sharedTransactionTemplate) {
        this.sharedTransactionTemplate = sharedTransactionTemplate;
    }

    @Override
    public void afterPropertiesSet() {
        if (transactionManager == null) {
            throw new IllegalArgumentException("Property 'transactionManager' is required");
        }
        if (delegate == null) {
            delegate = new SimpleAsyncTaskExecutor();
        }
        if (sharedTransactionTemplate == null) {
            sharedTransactionTemplate = new TransactionTemplate(transactionManager);
        }
    }
}

Это реализация есть обёртка, в итоге делигирующая вызовы к любому TaskExecutor коих несколько в составе Spring. При этом каждый вызов «завёрнут» в транзакцию. Вручную управлять транзакциями в Spring можно используя TransactionTemplate, а вот EntityManager#getTransaction() выдаёт исключение.

Ну и наконец около практический пример в действии:

Конфигурируем TaskExecutor:

    <bean id="transactionalTaskExecutor" class="ru.habrahabr.support.spring.DelegatedTransactionalAsyncTaskExecutor">
        <property name="delegate">
            <bean class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
                <property name="threadNamePrefix" value="Habrahabr example - "/>
                <property name="threadGroupName" value="Habrahabr examples Group"/>
                <property name="corePoolSize" value="10"/>
                <property name="waitForTasksToCompleteOnShutdown" value="true"/>
            </bean>
        </property>
        <property name="transactionManager" ref="transactionManager"/>
    </bean>

Пример сервиса:

@Service
public class FooServiceImpl implements FooService {
    @Autowired
    private TransactionalAsyncTaskExecutor trTaskExecutor;
    @Autowired
    private FooDao fooDao;

    @Transactional
    @Override
    public void update(Foo entity)  {
        fooDao.merge(entity);                                   // Выполнится в транзакции созданной Spring'ом (tr_1).
        trTaskExecutor.run(new Runnable() {       // Запустится новый поток и новая транзакция (tr_2), метод run() выполнится паралельно текущему потоку и в рамках транзакции tr_2.
            public void run() {
                someLongTimeOperation();
            }
        });
    }                                                                             // Выход из метода и вместе с этим tr_1 завершится. Обрабока tr_2 осуществится с помощью TransactionTemplate.
     
    @Transactional
    @Override
    public void someLongTimeOperation(Foo entity)  {
        // тут набор ресурсоёмких операций
    }
}

Таким образом, имеем вполне обобщённую реализацию обёртки для TaskExecutor, позволяющую избежать дублирования кода создания транзакций.

Автор: VocVark

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js