[Spring cloud 一步步實現廣告系統] 14. 全量索引程式碼實現

  • 2019 年 10 月 3 日
  • 筆記

上一節我們實現了索引基本操作的類以及索引快取工具類,本小節我們開始實現載入全量索引數據,在載入全量索引數據之前,我們需要先將資料庫中的表數據導出到一份文件中。Let’s code.

1.首先定義一個常量類,用來存儲導出文件存儲的目錄和文件名稱

因為我們導出的文件需要在搜索服務中使用到,因此,我們將文件名 & 目錄以及導出對象的資訊編寫在mscx-ad-commom項目中。

public class FileConstant {      public static final String DATA_ROOT_DIR = "/Users/xxx/Documents/promotion/data/mysql/";        //各個表數據的存儲文件名      public static final String AD_PLAN = "ad_plan.data";      public static final String AD_UNIT = "ad_unit.data";      public static final String AD_CREATIVE = "ad_creative.data";      public static final String AD_CREATIVE_RELARION_UNIT = "ad_creative_relation_unit.data";      public static final String AD_UNIT_HOBBY = "ad_unit_hobby.data";      public static final String AD_UNIT_DISTRICT = "ad_unit_district.data";      public static final String AD_UNIT_KEYWORD = "ad_unit_keyword.data";  }

2.定義索引對象導出的欄位資訊,依然用Ad_Plan為例。

/**   * AdPlanTable for 需要導出的表欄位資訊 => 是搜索索引欄位一一對應   *   * @author <a href="mailto:[email protected]">Isaac.Zhang | 若初</a>   */  @Data  @AllArgsConstructor  @NoArgsConstructor  public class AdPlanTable {      private Long planId;      private Long userId;      private Integer planStatus;      private Date startDate;      private Date endDate;  }

3.導出文件服務實現

同樣,最好的實現方式就是將導出服務作為一個子工程來獨立運行,我這裡直接實現在了mscx-ad-db項目中

  • 定義一個空介面,為了符合我們的編碼規範
/**   * IExportDataService for 導出資料庫廣告索引初始化數據   *   * @author <a href="mailto:[email protected]">Isaac.Zhang | 若初</a>   */  public interface IExportDataService {  }
  • 實現service
@Slf4j  @Service  public class ExportDataServiceImpl implements IExportDataService {        @Autowired      private AdPlanRepository planRepository;        /**       * 導出 {@code AdPlan} from DB to File       *       * @param fileName 文件名稱       */      public void exportAdPlanTable(String fileName) {          List<AdPlan> planList = planRepository.findAllByPlanStatus(CommonStatus.VALID.getStatus());          if (CollectionUtils.isEmpty(planList)) {              return;          }            List<AdPlanTable> planTables = new ArrayList<>();          planList.forEach(item -> planTables.add(                  new AdPlanTable(                          item.getPlanId(),                          item.getUserId(),                          item.getPlanStatus(),                          item.getStartDate(),                          item.getEndDate()                  )          ));            //將數據寫入文件          Path path = Paths.get(fileName);          try (BufferedWriter writer = Files.newBufferedWriter(path)) {              for (AdPlanTable adPlanTable : planTables) {                  writer.write(JSON.toJSONString(adPlanTable));                  writer.newLine();              }              writer.close();          } catch (IOException e) {              e.printStackTrace();              log.error("export AdPlanTable Exception!");          }      }  }
  • 實現Controller,提供操作入口
@Slf4j  @Controller  @RequestMapping("/export")  public class ExportDataController {      private final ExportDataServiceImpl exportDataService;        @Autowired      public ExportDataController(ExportDataServiceImpl exportDataService) {          this.exportDataService = exportDataService;      }        @GetMapping("/export-plan")      public CommonResponse exportAdPlans() {            exportDataService.exportAdPlanTable(String.format("%s%s", FileConstant.DATA_ROOT_DIR, FileConstant.AD_PLAN));          return new CommonResponse();      }  }
  • 結果文件內容如下,每一行都代表了一個推廣計劃
{"endDate":1561438800000,"planId":10,"planStatus":1,"startDate":1561438800000,"userId":10}  {"endDate":1561438800000,"planId":11,"planStatus":1,"startDate":1561438800000,"userId":10}
根據文件內容構建索引

我們在之前編寫索引服務的時候,創建了一些索引需要使用的實體對象類,比如構建推廣計劃索引的時候,需要使用到的實體對象com.sxzhongf.ad.index.adplan.AdPlanIndexObject,可是呢,我們在上一節實現索引導出的時候,實體對象又是common 包中的com.sxzhongf.ad.common.export.table.AdPlanTable,讀取出來文件中的數據只能反序列化為JSON.parseObject(p, AdPlanTable.class),我們需要將2個對象做相互映射才能創建索引資訊。

1.首先我們定義一個操作類型枚舉,代表我們每一次的操作類型(也需要對應到後期binlog監聽的操作類型

public enum OperationTypeEnum {      ADD,      UPDATE,      DELETE,      OTHER;        public static OperationTypeEnum convert(EventType type) {          switch (type) {              case EXT_WRITE_ROWS:                  return ADD;              case EXT_UPDATE_ROWS:                  return UPDATE;              case EXT_DELETE_ROWS:                  return DELETE;              default:                  return OTHER;          }      }  }

2.因為全量索引的載入和增量索引載入的本質是一樣的,全量索引其實就是一種特殊的增量索引,為了程式碼的可復用,我們創建統一的類來操作索引。

/**   * AdLevelDataHandler for 通用處理索引類   * 1. 索引之間存在層級劃分,也就是相互之間擁有依賴關係的劃分   * 2. 載入全量索引其實是增量索引 "添加"的一種特殊實現   *   * @author <a href="mailto:[email protected]">Isaac.Zhang | 若初</a>   */  @Slf4j  public class AdLevelDataHandler {        /**       * 實現廣告推廣計劃的第二層級索引實現。       * (第一級為用戶層級,但是用戶層級不參與索引,所以從level 2開始)       * 第二層級的索引是表示 不依賴於其他索引,但是可被其他索引所依賴       */      public static void handleLevel2Index(AdPlanTable adPlanTable, OperationTypeEnum type) {          // 對象轉換          AdPlanIndexObject planIndexObject = new AdPlanIndexObject(                  adPlanTable.getPlanId(),                  adPlanTable.getUserId(),                  adPlanTable.getPlanStatus(),                  adPlanTable.getStartDate(),                  adPlanTable.getEndDate()          );            //調用通用方法處理,使用IndexDataTableUtils#of來獲取索引的實現類bean          handleBinlogEvent(                      // 在前一節我們實現了一個索引工具類,來獲取注入的bean對象                  IndexDataTableUtils.of(AdPlanIndexAwareImpl.class),                  planIndexObject.getPlanId(),                  planIndexObject,                  type          );      }        /**       * 處理全量索引和增量索引的通用處理方式       * K,V代表索引的鍵和值       *       * @param index 索引實現代理類父級       * @param key   鍵       * @param value 值       * @param type  操作類型       */      private static <K, V> void handleBinlogEvent(IIndexAware<K, V> index, K key, V value, OperationTypeEnum type) {          switch (type) {              case ADD:                  index.add(key, value);                  break;              case UPDATE:                  index.update(key, value);                  break;              case DELETE:                  index.delete(key, value);                  break;              default:                  break;          }      }  }

3.讀取文件實現全量索引載入。

因為我們文件載入之前需要依賴另一個組件,也就是我們的索引工具類,需要添加上@DependsOn("indexDataTableUtils"),全量索引在系統啟動的時候就需要載入,我們需要添加@PostConstruct來實現初始化載入,被@PostConstruct修飾的方法會在伺服器載入Servlet的時候運行,並且只會被伺服器調用一次。

@Component  @DependsOn("indexDataTableUtils")  public class IndexFileLoader {        /**       * 服務啟動時,執行全量索引載入       */      @PostConstruct      public void init() {          //載入 推廣計劃          List<String> adPlanStrings = loadExportedData(String.format("%s%s",                  FileConstant.DATA_ROOT_DIR, FileConstant.AD_PLAN          ));          adPlanStrings.forEach(p -> AdLevelDataHandler.handleLevel2Index(                  JSON.parseObject(p, AdPlanTable.class), OperationTypeEnum.ADD          ));      }        /**       * <h3>讀取全量索引載入需要的文件</h3>       *       * @param fileName 文件名稱       * @return 文件行數據       */      private List<String> loadExportedData(String fileName) {          try (BufferedReader reader = Files.newBufferedReader(Paths.get(fileName))) {              return reader.lines().collect(Collectors.toList());          } catch (IOException e) {              throw new RuntimeException(e.getMessage());          }      }  }

Tips

在實現初始化載入全量索引的過程中,一定要保證數據載入的順序問題,因為不同的數據有可能存在著相互依賴的關聯關係,一旦順序寫錯,會造成程式報錯問題。