作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
上期文章:详解SpringCloud微服务技术栈:深入ElasticSearch(2)——自动补全、拼音搜索
订阅专栏:微服务技术全家桶
希望文章对你们有所帮助
数据同步,可以说是非常重要的,我看到的很多面经里面就有关于数据同步的问题。
在之前从MySQL中批量导入了酒店数据到ElasticSearch,因此当MySQL数据发生改变时,ElasticSearch也必须跟着改变,这就是ES与MySQL之间的数据同步。而之前在学习Redis的时候,这就是一个很重要的问题,即双写一致性问题。
如果是一个单体项目,那么MySQL数据怎么懂,ElasticSearch也相应做变化就好了,但是现在我们使用的是微服务架构,酒店管理(MySQL)的业务和搜索酒店(ElasticSearch)的业务很可能都不在同一个微服务下,数据同步就没办法直接实现。有三种实现方案:
方案一:同步调用
但是这种方式,admin调用了demo,业务耦合度太高了,影响性能。
方案二:异步通知
当有新增的数据时,依旧先写MySQL,写入后是将消息放入到消息队列中,至于谁需要这个消息,就与admin业务没有关系了,有效解除了耦合,是比较推荐的方案,但是这种方案比较依赖MQ的可靠性,实现复杂度会上升。
方案三:监听binlog
学习MySQL主从同步的时候有接触过,MySQL中的binlog默认关闭,一旦开启,每当MySQL在做修改的时候,都会将相应的操作记录在binlog中,那么就可以使用canal这种中间件来监听binlog,并通知demo做更新。
耦合度确实很低,但是会对MySQL产生一定的压力。
在这里将会使用方案二。
之前一直在做hotel-demo,现在要导入hotel-admin来作为酒店管理的微服务,当数据发生增删改的时候,要对ElasticSearch中的数据完成相同操作。
hotel-admin项目从网盘中下载并导入工程:
链接:https://pan.baidu.com/s/19gdxEBRxpAXdaozyw3uJGQ?pwd=rzbg
提取码:rzbg
之前学习MQ的时候,要发送消息,就需要指定好交换机exchange、队列queue以及RoutingKey。
ES增和改的时候,实际上都是一样的语句(id不存在就新增,id存在就修改),所以在ES操作中,增和改可以算成是一个业务。
因此消息队列只有2个,一个为hotel.insert.queue,一个为hotel.delete.queue,且每一个队列都有一个唯一的RoutingKey,交换机为hotel.topic。
声明队列和交换机一般都是在消费者中进行的。
1、在hotel-demo、hotel-admin中引入依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
2、在hotel-demo和hotel-admin配置中配置一下AMQP的地址:
3、hotel-demo与hotel-admin中定义一个常量类:
public class MqConstants {
//交换机
public final static String HOTEL_EXCHANGE = "hotel.topic";
//监听新增或修改的队列
public final static String HOTEL_INSERT_QUEUE = "hotel.insert.queue";
//监听删除的队列
public final static String HOTEL_DELETE_QUEUE = "hotel.delete.queue";
//新增或修改的RoutingKey
public final static String HOTEL_INSERT_KEY = "hotel.insert";
//删除的RoutingKey
public final static String HOTEL_DELETE_KEY = "hotel.delete";
}
4、在hotel-demo中基于bean的方式声明并绑定关系:
@Configuration
public class MqConfig {
@Bean
public TopicExchange topicExchange() {
return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false);
}
@Bean
public Queue insertQueue(){
return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true);
}
@Bean
public Queue deleteQueue(){
return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true);
}
@Bean
public Binding insertQueueBinding() {
return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);
}
@Bean
public Binding deleteQueueBinding() {
return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);
}
}
从hotel-admin中的增删改业务中增加消息发送到MQ的环节。原先这个项目导入的环节,业务逻辑都放在controller里面实现了,这种肯定是不规范的,业务逻辑应该全部放在service层中进行。我新增了Service层:
HotelController:
@PostMapping
public void saveHotel(@RequestBody Hotel hotel){
hotelService.saveHotel(hotel);
}
@PutMapping()
public void updateHotelById(@RequestBody Hotel hotel){
hotelService.updateHotelById(hotel);
}
@DeleteMapping("/{id}")
public void deleteHotelById(@PathVariable("id") Long id) {
hotelService.deleteHotelById(id);
}
IHotelService:
public interface IHotelService extends IService<Hotel> {
void saveHotel(Hotel hotel);
void updateHotelById(Hotel hotel);
void deleteHotelById(Long id);
}
实现类HotelService:
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
@Resource
private RabbitTemplate rabbitTemplate;
@Override
public void saveHotel(Hotel hotel) { //新增酒店并发送到消息队列
save(hotel);
rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE, MqConstants.HOTEL_INSERT_KEY, hotel.getId());
}
@Override
public void updateHotelById(Hotel hotel) { //修改酒店并发送到消息队列
if (hotel.getId() == null) {
throw new InvalidParameterException("id不能为空");
}
updateById(hotel);
rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE, MqConstants.HOTEL_INSERT_KEY, hotel.getId());
}
@Override
public void deleteHotelById(Long id) { //删除酒店并发送到消息队列
removeById(id);
rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE, MqConstants.HOTEL_DELETE_KEY, id);
}
}
消费者hotel-demo需要完成消息的监听并且更新ElasticSearch中的数据,而消息的监听都是通过编写实体类来实现的。
@Component
public class HotelListener {
@Resource
IHotelService hotelService;
/**
* 监听酒店新增或修改的业务
* @param id 酒店id
*/
@RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)
public void listenHotelInsertOrUpdate(Long id) {
hotelService.insertById(id);
}
/**
* 监听酒店删除的业务
* @param id 酒店id
*/
@RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE)
public void listenHotelDelete(Long id) {
hotelService.deleteById(id);
}
}
之前在HotelService中没有实现着两个方法,所以需要在实现类中用RestClient实现:
@Override
public void deleteById(Long id) {
try {
//准备request
DeleteRequest request = new DeleteRequest("hotel").id(id.toString());
//发送请求
client.delete(request, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void insertById(Long id) {
try {
//根据id查询酒店数据
Hotel hotel = getById(id);
//转换为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
//准备request
IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
//准备DSL
request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
//发送请求
client.index(request, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
重启hotel-demo和hotel-admin,查看MQ后台的交换机、消息队列以及他们是否绑定成功:
在酒店管理系统中修改上海希尔顿酒店的价格为2687,消息队列中存入了消息,且直接被消费者监听和消费掉了: