乐趣区

Spring-cloud-一步步实现广告系统-13-索引服务编码实现

上一节我们分析了广告索引的维护有 2 种,全量索引加载 增量索引维护。因为广告检索是广告系统中最为重要的环节,大家一定要认真理解我们索引设计的思路,接下来我们来编码实现索引维护功能。

我们来定义一个接口,来接收所有 index 的增删改查操作,接口定义一个范型,来接收 2 个参数,K代表我们索引的健值,V代表返回值。

/**
 * IIndexAware for 实现广告索引的增删改查
 *
 * @author <a href="mailto:magicianisaac@gmail.com">Isaac.Zhang | 若初 </a>
 */
public interface IIndexAware<K, V> {

    /**
     * 通过 key 获取索引
     */
    V get(K key);
    /**
     * 添加索引
     * @param key
     * @param value
     */
    void add(K key, V value);
    /**
     * 更新索引
     */
    void update(K key, V value);
    /**
     * 删除索引
     */
    void delete(K key, V value);
}

我们一定要知道,并不是所有的数据库表都需要创建索引,比如 User 表我们在数据检索的时候其实是不需要的,当然也就没必要创建索引,并且,也不是表中的所有字段都需要索引,这个也是根据具体的业务来确定字段信息,比如我们接下来要编写的 推广计划 索引中,推广计划名称就可以不需要。下面,我们来实现我们的第一个 正向索引

  • 首先创建操作推广计划的实体对象
/**
 * AdPlanIndexObject for 推广计划索引对象
 * 这个索引对象我们没有添加 推广计划名称
 * @author <a href="mailto:magicianisaac@gmail.com">Isaac.Zhang | 若初 </a>
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AdPlanIndexObject {

    private Long planId;
    private Long userId;
    private Integer planStatus;
    private Date startDate;
    private Date endDate;

    /**
    * 根据实际字段来更新索引
    */
    public void update(AdPlanIndexObject newObject) {if (null != newObject.getPlanId()) {this.planId = newObject.getPlanId();
        }
        if (null != newObject.getUserId()) {this.userId = newObject.getUserId();
        }
        if (null != newObject.getPlanStatus()) {this.planStatus = newObject.getPlanStatus();
        }
        if (null != newObject.getStartDate()) {this.startDate = newObject.getStartDate();
        }
        if (null != newObject.getEndDate()) {this.endDate = newObject.getEndDate();
        }
    }
}
  • 然后创建推广计划索引实现类,并实现 IIndexAware 接口。
/**
 * AdPlanIndexAwareImpl for 推广计划索引实现类
 *
 * @author <a href="mailto:magicianisaac@gmail.com">Isaac.Zhang | 若初 </a>
 */
@Slf4j
@Component
public class AdPlanIndexAwareImpl implements IIndexAware<Long, AdPlanIndexObject> {

    private static Map<Long, AdPlanIndexObject> planIndexObjectMap;

    /**
     * 因为操作索引的过程中有可能对索引进行更新,为了防止多线程造成的线程不安全问题,我们不能使用 hashmap,需要实现 ConcurrentHashMap
     */
    static {planIndexObjectMap = new ConcurrentHashMap<>();
    }

    @Override
    public AdPlanIndexObject get(Long key) {return planIndexObjectMap.get(key);
    }

    @Override
    public void add(Long key, AdPlanIndexObject value) {log.info("AdPlanIndexAwareImpl before add::{}", planIndexObjectMap);
        planIndexObjectMap.put(key, value);
        log.info("AdPlanIndexAwareImpl after add::{}", planIndexObjectMap);
    }

    @Override
    public void update(Long key, AdPlanIndexObject value) {log.info("AdPlanIndexAwareImpl before update::{}", planIndexObjectMap);
                // 查询当前的索引信息,如果不存在,直接新增索引信息
        AdPlanIndexObject oldObj = planIndexObjectMap.get(key);
        if (null == oldObj) {planIndexObjectMap.put(key, value);
        } else {oldObj.update(value);
        }

        log.info("AdPlanIndexAwareImpl after update::{}", planIndexObjectMap);
    }

    @Override
    public void delete(Long key, AdPlanIndexObject value) {log.info("AdPlanIndexAwareImpl before delete::{}", planIndexObjectMap);
        planIndexObjectMap.remove(key);
        log.info("AdPlanIndexAwareImpl after delete::{}", planIndexObjectMap);
    }
}

至此,我们已经完成了推广计划的索引对象和索引操作的代码编写,大家可以参考上面的示例,依次完成 推广单元 推广创意 地域 兴趣 关键词 以及 推广创意和推广单元的关联索引,或者可直接从 Github 传送门 / Gitee 传送门 下载源码。

按照上述代码展示,我们已经实现了所有的索引操作的定义,但是实际情况中,我们需要使用这些服务的时候,需要在每一个 Service 中 @Autowired 注入,我们那么多的索引操作类,还不包含后续还有可能需要新增的索引维度,工作量实在是太大,而且不方便维护,作为一个合格的程序员来说,这是非常不友好的,也许会让后续的开发人员骂娘。

为了防止后续被骂,我们来编写一个索引缓存工具类 com.sxzhongf.ad.index.IndexDataTableUtils,通过这个索引缓存工具类来实现一次注入,解决后顾之忧。要实现这个工具类,我们需要实现 2 个接口:org.springframework.context.ApplicationContextAwareorg.springframework.core.PriorityOrdered

  • org.springframework.context.ApplicationContextAware, 统一通过实现该接口的类,来操作 Spring 容器以及其中的 Bean 实例。
    在 Spring 中,以 Aware 为后缀结束的类,大家可以简单的理解为 应用程序想要 XXX,比如 ApplicationContextAware 代表应用程序想要ApplicationContext,BeanFactoryAware 表示应用程序想要BeanFactory… 等等
  • org.springframework.core.PriorityOrdered组件加载顺序,也可以使用org.springframework.core.Ordered

以下代码为我们的工具类:

package com.sxzhongf.ad.index;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.PriorityOrdered;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * IndexDataTableUtils for 所有索引服务需要缓存的 Java Bean
 *
 * 使用方式:* 获取 {@link com.sxzhongf.ad.index.creative.CreativeIndexAwareImpl} 索引服务类
 * 如下:* {@code
 *   IndexDataTableUtils.of(CreativeIndexAwareImpl.class)
 * }
 * @author <a href="mailto:magicianisaac@gmail.com">Isaac.Zhang | 若初 </a>
 */
@Component
public class IndexDataTableUtils implements ApplicationContextAware, PriorityOrdered {

    // 注入 ApplicationContext
    private static ApplicationContext applicationContext;

    /**
     * 定义用于保存所有 Index 的 Map
     * Class 标示我们的索引类
     */
    private static final Map<Class, Object> dataTableMap = new ConcurrentHashMap<>();

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {IndexDataTableUtils.applicationContext = applicationContext;}

    /**
     * 获取索引服务缓存
     */
    public static <T> T of(Class<T> klass) {T instance = (T) dataTableMap.get(klass);
        // 如果获取到索引 bean,直接返回当前 bean
        if (null != instance) {return instance;}
        // 首次获取索引 bean 为空,写入 Map
        dataTableMap.put(klass, bean(klass));
        return (T) dataTableMap.get(klass);
    }

    /**
     * 获取 Spring 容器中的 Bean 对象
     */
    private static <T> T bean(String beanName) {return (T) applicationContext.getBean(beanName);
    }

    /**
     * 获取 Spring 容器中的 Bean 对象
     */
    private static <T> T bean(Class klass) {return (T) applicationContext.getBean(klass);
    }

    @Override
    public int getOrder() {return PriorityOrdered.HIGHEST_PRECEDENCE;}
}
退出移动版