同学们好!在《我家租房网》的默认首页,提供了根据城区和入住日期搜索房源信息的功能。当用户用鼠标点击“选择城区”时,页面会弹出可供选择的城区列表,列表中的内容并非来自前端页面,而是由后端微服务从数据库中查询得到。这样一旦因业务调整导致房屋租赁服务的覆盖范围发生变化,只需修改数据库即可,对前端页面不会构成任何影响。另一方面,城区列表在相当长的时间内是不会发生变化的,每次显示都从MySQL数据库中查询显然效率不高,为此可以考虑利用Redis数据库的缓存机制,降低对MySQL数据库的访问频率,提高程序的整体运行性能。


细心的同学可能已经发现,前面几个业务功能,我们基本上都是先在前端服务器中予以实现,然后再移植到后端微服务中。这样做一方面是让大家了解采用传统单体架构的构建模式,及其向微服务架构演进的过程与所发生的变化,另一方面则是通过对两种架构模式的对比,更深刻地体会它们各自的优势与局限。从这节课开始,我们将跳过在前端服务器中实现业务逻辑的环节,直接采用微服务架构的构建模式,先实现后端微服务,再在前端服务器中添加路由处理函数并远程调用后端微服务,最后再将前后端连在一起做完整的功能测试。


首先我们来实现后端服务器。


第一步是创建特定的微服务。


在创建微服务之前,我们可以先执行这条SQL语句,将北京市下辖的十五个城区插入到城区表中。创建微服务的过程还是和先前一样,直接在back-end工程目录下执行go-micro命令,子命令为new,参数为service,微服务名为GetAreas。


接着,需要修改一下go-micro为我们自动生成的接口描述。在back-end工程目录下的GetAreas微服务目录中,有一个名为proto的子目录,这就是存放接口描述脚本及其Go代码文件的地方。现阶段里面只有一个名为GetAreas.proto的ProtoBuf脚本文件。我们需要对该文件做一些修改:

这段代码描述的是,前端服务器和后端微服务之间的数据交换格式。CallRequest表示前端提供给后端的请求数据,此处为空。CallResponse表示后端返回给前端的响应数据,其中包含错误代码、错误描述和城区列表,列表中的每个元素均包含城区ID和城区名两个属性。


有了用ProtoBuf语言描述的远程调用接口,我们还需要把它编译成基于Go语言的程序代码。在GetAreas微服务目录中执行make命令,并携带四个目标参数,它们是init、proto、update和tidy。


在GetAreas微服务目录下,创建一个名为model的子目录,表示该微服务的模型层。在该子目录中创建redis.go文件,封装所有与操作Redis数据库有关的代码:

这段代码在全局域定义一个Redis连接池对象,其它有关Redis的操作,我们随后再行添加。


在GetAreas微服务目录下的model子目录中,创建mysql.go文件,封装所有与操作MySQL数据库有关的代码。在实际编写数据库访问代码之前,我们可以先把之前为前端服务器编写的mysql.go文件中的内容原封不动地复制到这里:


修改GetAreas微服务目录中的main.go文件,添加对InitDB函数的调用,将服务发现改为Consul,绑定IP地址和端口:

程序启动伊始,即完成对数据库的初始化。其中包括两个动作,其一是连接ihomedb数据库并获得连接池对象,其二是在该数据库中创建表。当然,如果这些表已经存在,则不再创建。接着,在服务器对象的初始化部分,指示其注册到Consul服务器,同时绑定本机的9004端口。


接下来,我们为GetAreas微服务编写与数据持久化有关的代码。


打开GetAreas微服务目录下,model子目录中的mysql.go文件,在其中添加有关从MySQL数据库中读取城区列表的代码:

定义名为ReadAreas的函数,用于从MySQL数据库中读取城区列表。该函数通过数据库连接池对象的Find方法,查询areas表所有记录的所有字段,将查询结果输出到一个Area结构体类型的切片中并返回该切片。


这里我们借助Redis缓存从MySQL中查询到的城区列表,在该缓存的生命周期内,如果再次获取城区列表,可以直接从Redis中得到,避免因频繁访问MySQL而导致的性能下降。


打开GetAreas微服务目录下,model子目录中的redis.go文件,在其中添加有关从Redis数据库中加载城区列表和将城区列表保存到Redis数据库中的代码:

我们在这里先定义一个名为LoadAreas的函数,用于从Redis数据库中加载城区列表。注意从Redis中读到的城区列表是一个JSON格式的字符串,需要解码还原为Area结构体类型的切片并返回该切片。而后我们又定义了一个名为SaveAreas的函数,用于将城区列表保存到Redis数据库中。注意在保存之前,需要先将Area结构体切片形式的城区列表编码为JSON格式的字符串,再写入Redis。


在完成GetAreas微服务模型层的所有开发工作之后,我们将着手编写用于获取城区列表的业务代码。


打开GetAreas微服务目录下,handler子目录中的GetAreas.go文件,在其中的Call方法里,添加与获取城区列表有关的操作:

这里首先尝试从Redis数据库中加载城区列表,若加载失败,则从MySQL数据库中读取城区列表,并在读取成功后,将城区列表保存到Redis数据库中。最后遍历所得到的城区列表,将每个城区的ID和名字,添加到返回给前端服务器的响应对象中。


在完成GetAreas后端服务器的开发后,我们需要为前端服务器添加一条路由,并在路由处理函数中完成对后端微服务远程方法的调用。


前端和后端共用同一套接口描述。


因此,这里我们将GetAreas微服务目录下proto子目录中的所有文件,原封不动地复制到front-end工程目录proto子目录下的GetAreas目录中。


这里我们要为获取城区列表添加一条路由,GET方法结合/api/v1.0/areas路径,处理函数名为GetAreas。


在front-end工程目录下的main.go文件中添加一条路由:

这里我们调用了路由对象的GET方法,为获取城区列表添加了一条路由,将GET方法结合/areas路径,路由到controller包的GetAreas函数。将GetAreas函数定义在controller包里是因为该函数的主要任务是执行业务逻辑,属于MVC中的C,即控制器层的部分。


当然,在controller包里真的得有GetAreas函数。为此,我们打开front-end工程目录下controller子目录中的user.go文件。在该文件的import部分增加一行,同时定义GetAreas函数:

这段代码通过Consul服务发现,获取了有关名为getareas的微服务的信息,远程调用了其中的GetAreas对象的Call方法。该方法返回包含错误代码、错误描述和城区列表的响应对象。最后,将该响应对象序列化为一个JSON字符串,编码到HTTP响应中,回传给浏览器。


至此,我们已经完成获取城区列表的全部开发工作。下面我们将对这部分功能进行测试。


启动Consul服务器、GetAreas后端微服务和前端服务器。用浏览器打开《我家租房网》的默认首页,用鼠标点击“选择城区”时,页面弹出可供选择的城区列表。检查Redis数据库中的城区列表缓存,与MySQL数据库中的城区表完全一致。


谢谢大家,我们下节课再见!