成就係統實現(三)-架構設計
阿新 • • 發佈:2019-02-03
1.技術選型
之前上網找過一些關於成就係統設計的文件,推薦的都是事件驅動模型,因此底層設計也基於這個模型來做,由業務方產生可能觸發成就的事件,統一放到一個佇列裡面,由執行緒去取佇列裡面的事件,進行分發處理。
技術點:
1.1 事件驅動模型
1.2 disruptor 號稱百萬流量的一個高併發處理框架
2.資料庫設計
2.1 成就包配置表 CREATE TABLE `achievement_package` ( `achievement_package_id` varchar(32) NOT NULL COMMENT '主鍵id',`name` varchar(32) NOT NULL COMMENT '包名稱',
`start_time` bigint(20) NOT NULL COMMENT '開始時間',
`end_time` bigint(20) NOT NULL COMMENT '結束時間',
`create_time` bigint(20) NOT NULL COMMENT '建立時間',
`op_time` bigint(20) NOT NULL COMMENT '更新時間',
`last_ver` int(11) NOT NULL COMMENT '版本號',
`is_valid` tinyint(4) NOT NULL COMMENT '是否有效 0-失效 1-有效 ',
`name_code` varchar(32) NOT NULL DEFAULT '' COMMENT '成就包名國際化編碼',
PRIMARY KEY (`achievement_package_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成就包配置表'
2.2 成就模板表 CREATE TABLE `achievement_template` ( `achievement_template_id` varchar(32) NOT NULL COMMENT '主鍵id',
`name` varchar(32) NOT NULL COMMENT '成就名稱',
`name_code` varchar(32) NOT NULL COMMENT '成就名稱國際化編碼',
`context` varchar(64) DEFAULT NULL COMMENT '描述',
`context_code` varchar(64) DEFAULT NULL COMMENT '描述國際化編碼',
`reward` varchar(64) NOT NULL COMMENT '獎勵 json',
`pic_url` varchar(255) DEFAULT NULL COMMENT '成就圖片 status-url',
`conditions` text NOT NULL COMMENT '達成條件 json',
`achievement_package_id` varchar(32) NOT NULL COMMENT '所屬成就包',
`start_time` bigint(20) NOT NULL COMMENT '成就開始時間',
`end_time` bigint(20) NOT NULL COMMENT '成就結束時間',
`create_time` bigint(20) NOT NULL COMMENT '建立時間',
`op_time` bigint(20) NOT NULL COMMENT '更新時間',
`last_ver` int(11) NOT NULL COMMENT '版本號',
`is_valid` tinyint(4) NOT NULL COMMENT '是否有效 0-失效 1-有效 ',
PRIMARY KEY (`achievement_template_id`),
KEY `udx_ach_pack_id` (`achievement_package_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成就模板表' 2.3 成就記錄表 CREATE TABLE `achievement_record` (
`achievement_record_id` varchar(32) NOT NULL COMMENT '主鍵id',
`customer_register_id` varchar(32) DEFAULT NULL COMMENT '小二id',
`achievement_template_id` varchar(32) NOT NULL COMMENT '成就id',
`fire_seed` int(11) NOT NULL COMMENT '火種數',
`finish_time` bigint(20) NOT NULL COMMENT '完成時間',
`status` smallint(6) NOT NULL COMMENT '狀態 0-未開始 1-進行中 2-已完成 3-進行中已過期 4-未完成已過期',
`schedule` text COMMENT '進度',
`create_time` bigint(20) NOT NULL COMMENT '建立時間',
`op_time` bigint(20) NOT NULL COMMENT '更新時間',
`last_ver` int(11) NOT NULL COMMENT '版本號',
`is_valid` tinyint(4) NOT NULL COMMENT '是否有效 0-失效 1-有效 ',
`achievement_package_id` varchar(32) NOT NULL COMMENT '成就包Id',
PRIMARY KEY (`achievement_record_id`),
UNIQUE KEY `udx_cus_ach_id` (`customer_register_id`,`achievement_template_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='使用者成就記錄表'
3.主要類圖
主要類講解: 1.AchievementEvent (成就事件基類)
由基類派生出多個成就事件子類/** * 店鋪Id */ private String entityId; /** * 使用者Id集合 完成事件的人 */ private List<String> customerRegisterIds; /** * 事件來源 1-APP 2-H5 {@link AchievementConstant.EventSource} */ private int source; /** * 事件型別 see{@link AchievementEnum} */ private int type; /** * 成就Id (封裝每個handler裡面對應的成就Id) */ private String achievementId; /** * 佇列名 */ private String disruptorName;
2.EventClient (事件分發器)
這個類的作用將對應的事件分發給對應的事件釋出器處理 3.IEventPublisher(事件釋出)public class EventClient implements IEventClient, ApplicationContextAware { private ApplicationContext applicationContext; /** * 獲得上下文 * * @param applicationContext * @throws BeansException */ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } /** * 釋出事件 * * @param event * @return */ public Boolean publish(AchievementEvent event) { String eventName = StringUtils.uncapitalize(event.getClass().getSimpleName()); IEventPublisher publisher = (IEventPublisher) applicationContext.getBean(eventName + "Publisher"); if (publisher != null) { return publisher.publish(event); } return false; } }
public interface IEventPublisher<T extends AchievementEvent> {
/**
* 釋出事件
* @param event
* @return
*/
boolean publish(T event);
}
每種型別的事件都有各自的釋出器,實現這個介面
舉例:OrderEventPublisher(訂單事件釋出器)
public class OrderEventPublisher implements InitializingBean, IEventPublisher<OrderEvent> {
private static final EventTranslatorOneArg<OrderEvent, OrderEvent> translator = (OrderEvent event, long sequence,
OrderEvent arg0) -> {
event.setDisruptorName(arg0.getDisruptorName());
event.setEntityId(arg0.getEntityId());
event.setCustomerRegisterIds(arg0.getCustomerRegisterIds());
event.setOrderId(arg0.getOrderId());
event.setAction(arg0.getAction());
event.setOrderFee(arg0.getOrderFee());
event.setType(arg0.getType());
event.setSource(arg0.getSource());
event.setOrderType(arg0.getOrderType());
};
private static Logger log = LoggerFactory.getLogger(OrderEventPublisher.class);
private Disruptor<OrderEvent> disruptor;
@Resource
private WelcomeHandler welcomeHandler;
@Resource
private LoveProverbsHandler loveProverbsHandler;
@Resource
private TechnologyHouseHandler technologyHouseHandler;
@Resource
private CookeryGodPassHandler cookeryGodPassHandler;
private int bufferSize;
private RingBuffer ringBuffer;
@Override
public void afterPropertiesSet() throws Exception {
disruptor = new Disruptor<>(() -> new OrderEvent(),
4096, new EventHandlerThreadFactory("OrderEvent"), ProducerType.MULTI, new BlockingWaitStrategy());
disruptor.handleEventsWith(welcomeHandler, loveProverbsHandler, technologyHouseHandler).then(cookeryGodPassHandler);
disruptor.setDefaultExceptionHandler(new AchieveEventExceptionHandler("OrderEventDisruptor"));
ringBuffer = disruptor.start();
bufferSize = ringBuffer.getBufferSize();
}
@Override
public boolean publish(OrderEvent event) {
event.setDisruptorName("OrderEventDisruptor");
if (ringBuffer.remainingCapacity() < bufferSize * 0.01) {
log.warn("OrderEventDisruptor size = " + ringBuffer.remainingCapacity());
}
return ringBuffer.tryPublishEvent(translator, event);//釋出事件;
}
}
每個事件釋出器管理自己的一個disruptor佇列,存放對應的事件,在初始化的時候設定事件的監聽器,也就是對應的成就處理類
之後的publish就呼叫ringbuffer的方法,有興趣的可以研究下disruptor原始碼
4.AbstractAchieveHandler(成就處理基類)
/**
* 執行事件
*
* @param event
* @param sequence
* @param endOfBatch
* @throws Exception
*/
@Override
public void onEvent(AchievementEvent event, long sequence, boolean endOfBatch) throws Exception {
Logger.warn("{} {} handle" +
" event event = {}, 位於佇列序號={}", event.getDisruptorName(), this.getClass().getSimpleName(), event.toString(), sequence);
if (event == null || CollectionUtils.isEmpty(event.getCustomerRegisterIds())) {
LoggerUtil.error(FireMemberLoggerFactory.ACHIEVEMENT_LOGGER, LogMarker.ACHIEVEMENT_HANDLER,
"valid param fail event={}", event != null ? event.toString() : null);
return;
}
if (!checkEvent(event)) {
return;
}
for (String customerRegisterId : event.getCustomerRegisterIds()) {
AchievementDTO achievementDTO = checkAchievement(customerRegisterId, event);
if (achievementDTO.getStatus() == AchievementConstant.RecordStatus.STATUS_COMPLETED && reward(achievementDTO)) {
sendMessage(achievementDTO);
}
}
}
主要採用了模板設計模式,大部分功能在基類裡面定義好了,各自具體的成就類只需要繼承基類,實現checkEvent方法就行
舉例:WelComeHandler
public class WelcomeHandler extends AbstractAchieveHandler {
private static String achievementId = "c5a968a3372c474794ef670fe9ad7132";
@Override
protected boolean checkEvent(AchievementEvent event) {
//1.事件型別檢驗
if (event instanceof OrderEvent) {
OrderEvent orderEvent = (OrderEvent) event;
if (orderEvent.getAction() == AchievementEnum.OrderActionEnum.SUBMIT.getCode() &&
AchievementConstant.OrderType.ORDER_SHOP.equals(orderEvent.getOrderType())) {
event.setAchievementId(achievementId);
return true;
}
}
return false;
}
}
checkEvent方法主要是判斷成就是否達成,因為每個成就都有各自的條件,因此可變的部分由各自的子類去實現。
就此,大致的一個框架設計 就出來了。