上一节咱们实现了索引基本操做的类以及索引缓存工具类,本小节咱们开始实现加载全量索引数据,在加载全量索引数据以前,咱们须要先将数据库中的表数据导出到一份文件中。Let's code.java
1.首先定义一个常量类,用来存储导出文件存储的目录和文件名称mysql
由于咱们导出的文件须要在搜索服务中使用到,所以,咱们将文件名 & 目录以及导出对象的信息编写在
mscx-ad-commom
项目中。sql
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:magicianisaac@gmail.com">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
项目中bash
/**
* IExportDataService for 导出数据库广告索引初始化数据
*
* @author <a href="mailto:magicianisaac@gmail.com">Isaac.Zhang | 若初</a>
*/
public interface IExportDataService {
}
复制代码
@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!");
}
}
}
复制代码
@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监听的操做类型)app
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:magicianisaac@gmail.com">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.读取文件实现全量索引加载。this
由于咱们文件加载以前须要依赖另外一个组件,也就是咱们的索引工具类,须要添加上
@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
在实现初始化加载全量索引的过程当中,必定要保证数据加载的顺序问题,由于不一样的数据有可能存在着相互依赖的关联关系,一旦顺序写错,会形成程序报错问题。