Введение
всем привет. Не видел в день, например каждые 24 часа.
Я написал это на выходныхОдна статья приведет вас к земле DDD, я обнаружил, что все очень интересуются новыми областями и знаниями. Несколько статей серии DDD будут опубликованы позже, чтобы познакомить вас с некоторыми фактическими шагами, которые необходимо выполнить при миграции DDD mvc.
Блог серии DDD
- Одна статья приведет вас к земле DDD
- Событийная модель приземления DDD
- Складирование десанта DDD
- Многоуровневая архитектура лендинга DDD
Одна из концепций DDD заключается во всем, бизнес-границах и разделении. Когда я впервые не понял DDD, я подумал, что модель, управляемая событиями, может быть очень хороша для разделения системных функций. Конечно, это мое любимое блюдо, и я начал применять и глубже понимать событийно-ориентированную модель после того, как связался с DDD. На самом деле, будь то в среде Spring или в ежедневном процессе написания кода MVC, разумное использование модели, управляемой событиями, может значительно улучшить удобство сопровождения кода.
Таким образом, в этой статье будет представлено систематическое введение в использование модели, управляемой событиями, и подводные камни в DDD. Начиная с уровня приложений, это помогает каждому лучше выполнять миграцию архитектуры.
Мой первый буклет Nuggets«Углубленный ДДД»Он уже онлайн в Наггетс, приглашаем всех попробовать~
Я также создал группу WeChat DDD, так как QR-код нельзя разместить в статье, вы можете добавить меня в WeChat.baiyan_lou
, Обратите внимание на обмен DDD, я втяну вас в группу, добро пожаловать на обмен и вместе добивайтесь прогресса.
2. Модель, управляемая событиями
2.1 Зачем нужна модель, управляемая событиями
Прежде чем использовать фреймворк и технологию, вы должны сначала узнать, в каких бизнес-сценариях нужно использовать эту вещь. Зачем и как его использовать важнее.
Предположим, теперь у нас есть относительно большая система заказов с одной услугой со следующими бизнес-требованиями:创建订单后,需要下发优惠券,给用户增长积分
Во-первых, давайте посмотрим, как большинство студентов пишут в одном сервисе. [Предположим, что заказы, купоны и баллы — это независимые услуги]
//在orderService内部定义一个放下
@Transactional(rollbackFor = Exception.class)
public void createOrder(CreateOrderCommand command){
//创建订单
Long orderId = this.doCreate(command);
//发送优惠券
couponService.sendCoupon(command,orderId);
//增长积分
integralService.increase(command.getUserId,orderId);
}
Будут ли какие-либо проблемы с запуском вышеуказанного кода онлайн? Не буду!
Так зачем его менять?
Причина в том, что в процессе непрерывной итерации бизнес-требований основной бизнес-процесс, не сильно связанный с текущим бизнесом, может быть заменен или модернизирован в любое время.
Продвижение Double 11, пользователям необходимо дать каждому пользователю несколько небольших подарков при оформлении заказа, затем необходимо написать еще одну функцию, которая сращивается после основного метода. Дубль 11 закончился, этот код нужно закомментировать. Идет большая акция на год, подарок меняется, а код приходится добавлять обратно. . . .
Взад и вперед логика порядка становится вонючей и длинной, а большое количество логики кода с комментариями нелегко читать и понимать.
Если используется модель, управляемая событиями, то при успешном создании заказа на первом этапе публикуется событие предметной области, указывающее, что заказ успешно создан. Служба купонов, поощрительная служба, дарение подарков и т. д. отслеживают это событие и соответствующим образом обрабатывают отслеживаемое событие.
код управляемой событиями модели
//在orderService内部定义一个放下
@Transactional(rollbackFor = Exception.class)
public void createOrder(CreateOrderCommand command){
//创建订单
Long orderId = this.doCreate(command);
publish(orderCreateEvent);
}
//各个需要监听的服务
public void handlerEvent(OrderCreateEvent event){
//逻辑处理
}
Разделение кода в соответствии с принципом открытости-закрытости
2.2. Выбор модели на основе событий
2.2.1 Управляемый временем механизм в JDK
JDK предоставляет нам управляемые событиями (EventListener、EventObject
), режим наблюдателя (Observer
).
JDK не только предоставляетObservable
своего рода,Observer
Интерфейс поддерживает шаблон наблюдателя, а также предоставляетEventObject
,EventListener
Интерфейс для поддержки режима прослушивания событий.
Наблюдатель (Observer) эквивалентен прослушивателю событий (listener), наблюдаемое (Observable
) эквивалентен источнику события и событию.Когда логика выполняется, наблюдатель может быть уведомлен о запуске обновления наблюдателя, и наблюдатель и параметры могут быть переданы одновременно. Упрощенная реализация шаблона прослушивания событий
// 观察者,实现此接口即可
public interface Observer {
/**
* 当被观察的对象发生变化时候,这个方法会被调用
* Observable o:被观察的对象
* Object arg:传入的参数
**/
void update(Observable o, Object arg);
}
// 它是一个Class
public class Observable {
// 是否变化,决定了后面是否调用update方法
private boolean changed = false;
// 用来存放所有`观察自己的对象`的引用,以便逐个调用update方法
// 需要注意的是:1.8的jdk源码为Vector(线程安全的),有版本的源码是ArrayList的集合实现;
private Vector<Observer> obs;
public Observable() {
obs = new Vector<>();
}
public synchronized void addObserver(Observer o); //添加一个观察者 注意调用的是addElement方法, 添加到末尾 所以执行时是倒序执行的
public synchronized void deleteObserver(Observer o);
public synchronized void deleteObservers(); //删除所有的观察者
// 循环调用所有的观察者的update方法
public void notifyObservers();
public void notifyObservers(Object arg);
public synchronized int countObservers() {
return obs.size();
}
// 修改changed的值
protected synchronized void setChanged() {
changed = true;
}
protected synchronized void clearChanged() {
changed = false;
}
public synchronized boolean hasChanged() {
return changed;
}
}
Очередь внутреннего наблюдателя передаетсяObservable
справиться с этим,并且,它是线程安全的
. Однако этот метод не так удобен в использовании, нет шины сообщений, и вам нужно самостоятельно поддерживать наблюдателя и наблюдаемое. Для бизнес-систем также необходимо вести добавление каждого наблюдателя в отдельности.
2.2.2 Управляемый событиями механизм в spring
пружина обеспечивает после 4.2@EventListener
Аннотация позволяет более удобно использовать мониторинг.
Учащиеся, знакомые с процессом запуска Spring, знают, что контейнер Spring будет выпущен после его обновления.ContextRefreshedEvent
событие, поэтому, если нам нужно отслеживать это событие, мы можем напрямую написать класс слушателя.
@Slf4j
@Component
public class ApplicationRefreshedEventListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
//解析这个事件,做你想做的事,嘿嘿
}
}
Точно так же мы можем сами определить событие,ApplicationEventPublisher
Отправить.
/**
* 领域事件基类
*
* @author baiyan
* @date 2021/09/07
*/
@Getter
@Setter
@NoArgsConstructor
public abstract class BaseDomainEvent<T> implements Serializable {
private static final long serialVersionUID = 1465328245048581896L;
/**
* 领域事件id
*/
private String demandId;
/**
* 发生时间
*/
private LocalDateTime occurredOn;
/**
* 领域事件数据
*/
private T data;
public BaseDomainEvent(String demandId, T data) {
this.demandId = demandId;
this.data = data;
this.occurredOn = LocalDateTime.now();
}
}
Определите единую бизнес-шину для отправки событий
/**
* 领域事件发布接口
*
* @author baiyan
* @date 2021/09/07
*/
public interface DomainEventPublisher {
/**
* 发布事件
*
* @param event 领域事件
*/
void publishEvent(BaseDomainEvent event);
}
/**
* 领域事件发布实现类
*
* @author baiyan
* @date 2021/09/07
*/
@Component
@Slf4j
public class DomainEventPublisherImpl implements DomainEventPublisher {
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void publishEvent(BaseDomainEvent event) {
log.debug("发布事件,event:{}", event.toString());
applicationEventPublisher.publishEvent(event);
}
}
прослушать событие
@Component
@Slf4j
public class UserEventHandler {
@EventListener
public void handleEvent(DomainEvent event) {
//doSomething
}
}
Уху, взлетай~
По сравнению с управляемой событиями моделью наблюдателя, предоставляемой JDK, метод, предоставляемый spring, — это yyds.
2.3 Управление транзакциями на основе событий
Обычно мы публикуем событие после завершения хранения некоторых данных. В последующем мы запишем запись операции в запись es, но в это время у es может истечь время ожидания ответа кластера, и хранилище записей операций не смогло сообщить об ошибке. Однако с точки зрения бизнес-логики сбой хранения записей операций не должен влиять на логику выполнения основного процесса, требующего независимости от транзакций. Или, если есть ошибка в выполнении основного процесса, то нам нужно вызвать событие для отправки сообщений DingTalk в группу для онлайн-мониторинга бизнеса, и нам нужно бросить исключение в логике основного метода, а затем вызвать это событие. В это время, если мы используем@EventListener
, реализация вышеперечисленных бизнес-сценариев представляет собой более хлопотную логику.
Для решения вышеуказанных проблем Spring предоставляет нам два способа:
(1)@TransactionalEventListener
аннотация.
(2) Менеджер синхронизации транзакцийTransactionSynchronizationManager
.
Эта статья направлена на@TransactionalEventListener
Проведите анализ.
Сразу видно из названия, что этоEventListener
, в Spring 4.2+ есть метод, называемый@TransactionEventListener
Метод может реализовать обработку события при управлении транзакцией.
//被@EventListener标注,表示它能够监听事件
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EventListener
public @interface TransactionalEventListener {
//表示当前事件跟随消息发送方事务的出发时机,默认为消息发送方事务提交之后才进行处理。
TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;
//true时不论发送方是否存在事务均出发当前事件处理逻辑
boolean fallbackExecution() default false;
//监听的事件具体类型,还是建议指定一下,避免监听到子类的一些情况出现
@AliasFor(annotation = EventListener.class, attribute = "classes")
Class<?>[] value() default {};
//指向@EventListener对应的值
@AliasFor(annotation = EventListener.class, attribute = "classes")
Class<?>[] classes() default {};
//指向@EventListener对应的值
String condition() default "";
}
public enum TransactionPhase {
// 指定目标方法在事务commit之前执行
BEFORE_COMMIT,
// 指定目标方法在事务commit之后执行
AFTER_COMMIT,
// 指定目标方法在事务rollback之后执行
AFTER_ROLLBACK,
// 指定目标方法在事务完成时执行,这里的完成是指无论事务是成功提交还是事务回滚了
AFTER_COMPLETION
}
Мы знаем, что механизм прослушивания событий Spring (модель публикации и подписки) на самом деле не является асинхронным (по умолчанию), а синхронным для разделения кода. и@TransactionEventListener
Он по-прежнему таким, но для его решения добавлен метод обратного вызова, чтобы событие могло быть обработано только тогда, когда транзакция Commited, Rollback и т. д., для достижения цели синхронизации транзакций.
3. Тренируйтесь и ступайте на яму
Для событийно-ориентированной модели@TransactionEventListener
и@EventListener
Предположим, два бизнес-сценария.
Добавьте новых пользователей, свяжите роли и добавьте связанные записи операций авторизации ролей.
1.统一事务
: Вышеупомянутые три операции и транзакции интегрированы.Независимо от того, какая из них является ненормальной, данные будут откатываться единообразно.
2独立事务
: Вышеупомянутые три операции независимы. После того, как событие будет выпущено, любые последующие исключения не будут затронуты.
3.1. Единая транзакция
Пользователь добавлен
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
DomainEventPublisher domainEventPublisher;
@Transactional(rollbackFor = Exception.class)
public void createUser(){
//省略非关键代码
save(user);
domainEventPublisher.publishEvent(userEvent);
}
}
Ассоциация роли пользователя
@Component
@Slf4j
public class UserEventHandler {
@Autowired
DomainEventPublisher domainEventPublisher;
@Autowired
UserRoleService userRoleService;
@EventListener
public void handleEvent(UserEvent event) {
log.info("接受到用户新增事件:"+event.toString());
//省略部分数据组装与解析逻辑
userRoleService.save(userRole);
domainEventPublisher.publishEvent(userRoleEvent);
}
}
Запись операции роли пользователя
@Component
@Slf4j
public class UserRoleEventHandler {
@Autowired
UserRoleRecordService userRoleRecordService;
@EventListener
public void handleEvent(UserRoleEvent event) {
log.info("接受到userRole事件:"+event.toString());
//省略部分数据组装与解析逻辑
userRoleRecordService.save(record);
}
}
Выше приведена логика той же транзакции.Если в каком-либо методе возникнет исключение, вся логика вставки данных будет отменена.
дать заключение,@EventListener
Отмеченный метод добавляется в логику выполнения текущей транзакции и интегрируется с транзакцией основного метода.
Шаг в яму 1:
Строго говоря, это не для того, чтобы отделить основную логику от бизнеса, или в синхронной транзакции.Конечно, это тоже имеет сценарии адаптации.Можно сделать это ради простоты кода и ясности логики на уровне функций. Но это не так DDD.Метод службы приложений в DDD - это вариант использования, который проходит через логику основного процесса.Поскольку это бизнес с сильной согласованностью в текущей системе, он должен быть отражен в службе приложений . Конечно, это деловая граница. Например, пользователи и наделение полномочиями явно не являются строго согласованными операциями. Сбой наделения полномочиями не должен влиять на моих новых пользователей. Поэтому в этом сценарии не рекомендуется использовать унифицированные транзакции для преобразования DDD.
Ступайте на яму 2:
Логика выполнения в прослушивателе может занимать много времени и должна обрабатываться асинхронно.UserEventHandler
метка на методе@Async
, то он изолируется от транзакции метода основной логики, а транзакция в слушателе начинает быть независимой и не будет влиять на транзакцию в userService. Например, когда другие коды остаются неизменными, код службы роли пользователя изменяется следующим образом.
@Component
@Slf4j
public class UserEventHandler {
@Autowired
DomainEventPublisher domainEventPublisher;
@Autowired
UserRoleService userRoleService;
@EventListener
@Async
public void handleEvent(UserEvent event) {
log.info("接受到用户新增事件:"+event.toString());
//省略部分数据组装与解析逻辑
userRoleService.save(userRole);
domainEventPublisher.publishEvent(userRoleEvent);
throw new RuntimeException("制造一下异常");
}
}
Выяснилось, что пользователь добавлен, отношение роли пользователя добавлено, но запись операции не добавлена. Первый результат понятен, а вот второй странный: в обработчике событий было выброшено исключение, но данные успешно сохранились.
Это на самом деле потому, чтоUserEventHandler
изhandleEvent
Внешний слой метода вложен@Transactional
,userRoleService.save
После завершения операции транзакция фиксируется, и последующие исключения не затрагиваются. Чтобы сохранить согласованность транзакции, добавьте@Transactional
Вот и все.
3.2. Независимые дела
@EventListener в качестве драйвера для загрузки децентрализованного управления кодом — это неплохо. Однако на уровне DDD данные транзакций смешиваются между собой.Помимо проблем, найти слой за слоем проблематично, а пакетов данных много.Рекомендуется использовать его.@TransactionalEventListene
Пользователь добавлен
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
DomainEventPublisher domainEventPublisher;
@Transactional(rollbackFor = Exception.class)
public void createUser(){
//省略非关键代码
save(user);
domainEventPublisher.publishEvent(userEvent);
}
}
Ассоциация роли пользователя
@Component
@Slf4j
public class UserEventHandler {
@Autowired
DomainEventPublisher domainEventPublisher;
@Autowired
UserRoleService userRoleService;
@TransactionalEventListener
public void handleEvent(UserEvent event) {
log.info("接受到用户新增事件:"+event.toString());
//省略部分数据组装与解析逻辑
userRoleService.save(userRole);
domainEventPublisher.publishEvent(userRoleEvent);
}
}
Запись операции роли пользователя
@Component
@Slf4j
public class UserRoleEventHandler {
@Autowired
UserRoleRecordService userRoleRecordService;
@TransactionalEventListener
public void handleEvent(UserRoleEvent event) {
log.info("接受到userRole事件:"+event.toString());
//省略部分数据组装与解析逻辑
userRoleRecordService.save(record);
}
}
тот же код, измените аннотацию с@EventListener
заменить@TransactionalEventListener
. После казни обнаружилась волшебная проблема,Запись операции роли пользователяДанные не сохраняются! ! !
Взгляните на логику и измените аннотацию, и возникает эта проблема.Сравните разницу между двумя аннотациями.@TransactionalEventListener
Независимо от транзакции, с аннотациями по умолчаниюphase
Значение параметраTransactionPhase.AFTER_COMMIT, то есть метод основной логики выполняется после фиксации транзакции. И мы знаем, что код ключа фиксации транзакции весной находится вAbstractPlatformTransactionManager.commitTransactionAfterReturning
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
}
//断点处
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}
Добавьте следующую конфигурацию в файл конфигурации
logging:
level:
org:
mybatis: debug
Поставьте точку останова на место вышеприведенного кода и снова выполните логику.
нашел, впервыеuserService
Сохраните данные в эту точку останова, затем вuserRoleService.save
Логика, здесь не прописана точка останова и не прописан метод обработки события последующей записи операции.
Давайте посмотрим на журнал
- 2021-09-07 19:54:38.166, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
- 2021-09-07 19:54:38.166, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@77a74846]
- 2021-09-07 19:54:38.167, DEBUG, [,,], [http-nio-8088-exec-6], o.m.s.t.SpringManagedTransaction - JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1832a0d9] will be managed by Spring
- 2021-09-07 19:54:38.184, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@77a74846]
- 2021-09-07 19:54:51.423, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@77a74846]
- 2021-09-07 19:54:51.423, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@77a74846]
- 2021-09-07 19:54:51.423, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@77a74846]
- 2021-09-07 19:54:51.430, INFO, [,,], [http-nio-8088-exec-6], com.examp.event.demo.UserEventHandler - 接受到用户新增事件:com.examp.event.demo.UserEvent@385db2f9
- 2021-09-07 19:54:53.602, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
- 2021-09-07 19:54:53.602, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@9af2818] was not registered for synchronization because synchronization is not active
- 2021-09-07 19:54:53.603, DEBUG, [,,], [http-nio-8088-exec-6], o.m.s.t.SpringManagedTransaction - JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1832a0d9] will be managed by Spring
- 2021-09-07 19:54:53.622, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@9af2818]
Обратите внимание на получение用户新增事件
После журнала,SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@9af2818] was not registered for synchronization because synchronization is not active
Объясните, что текущее событие является логикой отсутствия выполнения транзакции. оглянуться назад снова@TransactionalEventListener
, конфигурация по умолчанию такова, что событие выполняется после фиксации транзакции, но здесь нет транзакций, поэтому, естественно, событие не будет запущено.
Посмотрите на картинку и посмотрите на логику кода
Итак, как решить вышеуказанную проблему?
На самом деле, эта вещь относительно проста:
1. Вы можете обозначить логику, которая слушает это событие, не задумываясь@TransactionalEventListener(fallbackExecution = true)
, событие запускается независимо от того, есть ли у отправителя события транзакция.
2. Отметьте одно поверх второго события публикации.@Transactional(propagation = Propagation.REQUIRES_NEW)
, помните, что не следует прямо помечать@Transactional
, так как транзакция в userService была зафиксирована, и@Transactional
Механизм распространения транзакций по умолчанию:Propagation.REQUIRED
, Если текущей транзакции нет, создать новую транзакцию, если транзакция уже есть, присоединиться к транзакции.
Транзакция в userService все еще существует, но она была зафиксирована и не может быть добавлена, а значит, запись операции все еще не может быть вставлена.
Измените конфигурацию на
logging:
level:
org: debug
Лог можно посмотреть
- 2021-09-07 20:26:29.900, DEBUG, [,,], [http-nio-8088-exec-2], o.s.j.d.DataSourceTransactionManager - Cannot register Spring after-completion synchronization with existing transaction - processing Spring after-completion callbacks immediately, with outcome status 'unknown'
4. Приложения, управляемые событиями, в DDD
После понимания модели, управляемой событиями в Spring, все, что нам нужно сделать, — это начать отделять бизнес-логику.
Уточнение бизнес-вариантов использования с помощью штормов событий, проектирования и полных сводных корней【ps:其实我觉得设计聚合根是最难的,业务边界是需要团队成员达成共识的地方,不是研发说了算的】
, разделить границы бизнес-домена и выполнить различные логики, изначально смешанные в сервисе, в соответствии с корнем агрегации:
- Для каждой командной операции агрегации выпускается как минимум одно предметное событие, указывающее на результат выполнения операции
- Каждое событие домена будет сохранено в хранилище событий
- Когда агрегат извлекается из репозитория, агрегат перестраивается на основе событий, произошедших с агрегатом, и события воспроизводятся в том же порядке, в котором они были созданы.
- Агрегированный снимок: Сериализация и сохранение моментального снимка состояния агрегированного события при его возникновении.
5. Резюме
В этом документе основное внимание уделяется концепции и применению модели, управляемой событиями, а также анализу и устранению ошибок в реальной бизнес-логике, которые могут возникнуть. Наконец, в нем анализируется, как реализовать приведенную выше модель, управляемую событиями, в DDD.
Конечно, я думаю, что у всех должно быть четкое представление о модели событий здесь, но применение в DDD все еще немного расплывчато. Тысячи слов слились в одно предложение:Связано с базовой логикой агрегации, используйте оркестровку службы приложений, не связанную с базовой логикой, следуйте модели, управляемой событиями, и используйте независимый режим транзакций.. Что касается согласованности данных, то это решается в соответствии с вашим собственным бизнесом, и методы и подводные камни вам были сообщены.
Мы с тобой оба архитекторы! ! !
6. Цитаты и ссылки
Принцип использования и реализации @TransactionalEventListener
7. Свяжитесь со мной
Если есть неточности в тексте, поправьте меня, текст писать непросто, ставьте лайк, ладно~
Диндин: louyanfeng25
WeChat: baiyan_lou
Общественный номер: дядя Бай Ян