同学们好!在上一节课,我们为大家介绍了《我家租房网》应用的用户注册流程。当注册页面打开时,浏览器向服务器发送图片ID,服务器向浏览器发送验证码图片,同时将图片ID和验证码文本,以键值对的形式保存起来。当用户获取短信验证码时,浏览器向服务器发送图片ID、用户输入的图片验证码文本和手机号。服务器根据图片ID,从存储中获得正确的图片验证码文本,验证用户的输入是否正确。如果正确,则随机生成一个短信验证码,发到用户的手机上,同时将手机号和短信验证码,以键值对的形式保存起来。当用户提交注册表单时,浏览器向服务器发送手机号、用户输入的短信验证码和密码。服务器根据手机号,从存储中获得正确的短信验证码,验证用户的输入是否正确。如果正确,则将用户的手机号和密码哈希值保存到数据库中,完成注册。从这段叙述中,我们不难发现,整个用户注册流程,共涉及到五次数据存取。第一次是在打开注册页面时,服务器要将图片ID和图片验证码文本,以键值对的形式保存起来。第二次和第三次是在获取短信验证码时,服务器一方面要根据图片ID,从存储中获得正确的图片验证码文本,另一方面还要将手机号和短信验证码,以键值对的形式保存起来。第四次和第五次是在提交注册表单时,服务器一方面要根据手机号,从存储中获得正确的短信验证码,另一方面还要将用户的手机号和密码哈希值保存到数据库中。在这五次数据存取中,只有最后一次是针对关系型数据的持久化数据存取,存储介质可以选择类似MySQL这样的关系型数据库。而前四次,其实都是针对键值对形式数据的临时性存取。象图片ID——图片验证码文本键值对,或者手机号——短信验证码键值对,这些数据仅在注册过程中有效,并不需要永久保存,选择类似Redis这样的,非关系型内存数据库作为存储介质更为合理。这节课我们要关注的重点,就是基于Redis的内存数据库操作,及其在微服务中的集成。
我们首先对Redis做一个快速入门,然后了解一下基于Go语言的Redis数据库访问,并实际编写一个简单的测试程序,最后将保存图片ID——图片验证码文本键值对的操作,添加到GetImageCode微服务中。
我们首先对Redis做一个快速入门。
Redis的安装包可以从其官网或GitHub上直接下载,其64位Windows版本是一个类似Redis-x64-3.2.100.msi的安装文件。象安装其它软件一样,双击该文件,遵循安装向导的提示,一步步完成安装即可。有关Redis的配置信息都集中在它的配置文件中。Windows版Redis的配置文件位于其安装目录下,名为redis.windows.conf。打开该文件可以看到这样一些配置项。bind和port表示Redis服务器绑定的地址和端口,默认为127.0.0.1和6379。
dbfilename表示持久化存储的文件名。默认为dump.rdb,保存数据快照,体积小,数据恢复快,性能高,但丢失率高,耐久性差。也可以人为指定扩展名为“.aof”的文件,保存操作日志,丢失率低,耐久性高,但体积大,数据恢复慢,性能差。
Redis的底层存储引擎采用红黑树结构,其Windows版,服务器程序名为redis-server.exe,客户机程序名为redis-cli.exe。我们可以直接在控制台启动Redis客户机,通过交互命令访问Redis数据库。比如通过set命令添加键值对或修改键的值,通过get命令获取键的值,通过keys命令获取键列表,通过flushall命令清空数据库,通过exit命令退出客户机程序。
作为应用程序的开发者,我们更关心的,当然是在Go语言程序中,如何访问Redis数据库。
在Redis官方提供的客户端资源网站上,我们可以看到面向不同编程语言的客户端软件包。其中的Redigo,就是针对Go语言的Redis客户端库。
我们可以从GitHub上下载Redigo,并解压到正确的路径下。
Redigo提供了一套非常简单易用的API,用于在Go语言程序中访问Redis数据库。连接数据库可以调用Dial函数,其参数中包括协议、地址和端口。操作数据库可以调用Do函数,其参数中包括具体的操作指令和数据。该函数返回操作结果。如果想从操作结果中,获得特定类型的数据,还需要调用一系列与类型具体化有关的函数。
下面我们将编写一个基于Go语言的测试程序,体验一下通过Redigo访问Redis数据库的基本方法。
我们在front-end工程目录下的test子目录中创建一个名为redis.go的文件:
xxxxxxxxxx
771package main
2
3import (
4 "fmt"
5 "github.com/gomodule/redigo/redis"
6)
7
8func main() {
9 // 连接数据库
10 conn, err := redis.Dial("tcp", "127.0.0.1:6379")
11 if err != nil {
12 fmt.Println("Redis错误:", err)
13 return
14 }
15
16 // 关闭数据库连接
17 defer conn.Close()
18
19 // 操作数据库
20 reply, err := conn.Do("set", "name", "zhangfei")
21 if err != nil {
22 fmt.Println("Redis错误:", err)
23 return
24 }
25
26 // 类型具体化
27 result, err := redis.String(reply, err)
28 if err != nil {
29 fmt.Println("Redis错误:", err)
30 return
31 }
32 fmt.Println(result)
33
34 // 操作数据库同时类型具体化
35 name, err := redis.String(conn.Do("get", "name"))
36 if err != nil {
37 fmt.Println("Redis错误:", err)
38 return
39 }
40 fmt.Println(name)
41
42 // 简化书写
43 if _, err := conn.Do("set", "age", 25); err != nil {
44 fmt.Println("Redis错误:", err)
45 return
46 }
47
48 // 操作数据库同时类型具体化
49 age, err := redis.Int(conn.Do("get", "age"))
50 if err != nil {
51 fmt.Println("Redis错误:", err)
52 return
53 }
54 fmt.Println(age)
55
56 // 列出所有键
57 keys, err := redis.Strings(conn.Do("keys", "*"))
58 if err != nil {
59 fmt.Println("Redis错误:", err)
60 return
61 }
62 fmt.Println(keys)
63
64 // 清空数据库
65 if _, err := conn.Do("flushall"); err != nil {
66 fmt.Println("Redis错误:", err)
67 return
68 }
69
70 // 列出所有键
71 keys, err = redis.Strings(conn.Do("keys", "*"))
72 if err != nil {
73 fmt.Println("Redis错误:", err)
74 return
75 }
76 fmt.Println(keys)
77}
在这段代码中,我们首先通过Redigo提供的Dial函数,建立与Redis数据库的连接。传给该函数的参数中包括所使用的通信协议,及Redis服务器的IP地址和端口号。Dial函数会返回一个连接对象,后续对数据库的所有操作,包括关闭数据库连接,都会用到这个连接对象。接着,我们通过连接对象的Do方法,在数据库中添加了一个键值对,键为“name”值为“zhangfei”。该函数返回两个值,一个应答对象和一个错误对象。应答对象需要通过Redigo的String函数具体化为字符串类型后,才能被当作字符串处理。我们也可以将这两步合二为一,将Do方法的返回值直接传给String函数,一次性得到具体类型的处理结果。比如后面获取“name”键的值,我们就是这样做的。对于某些操作,比如添加键值对或修改键的值,我们其实并不关心应答对象的具体内容,只需根据错误对象是否为空判断出错与否即可。这时我们可以采用简化书写的形式,代码看上去会更简洁。如果应答对象中包含的是一个整型数据,类型具体化应使用Redigo的Int函数,而如果是字符串切片,则使用Strings函数。数据库一旦被清空,所有的键值对都会立刻消弭于无形。
最后,我们将把保存图片ID——图片验证码文本键值对的操作,添加到GetImageCode微服务中。
回想一下上节课,我们为GetImageCode微服务所定义的接口描述。其中表示前端提供给后端的请求数据的CallRequest,只是一对空的花括号。那是因为那时我们的前端确实不需要为后端提供什么。但现在的情况不同了。我们既然要在后端,将图片ID——图片验证码文本键值对,保存到Redis数据库中,我们就必须先拿到图片ID,该信息只能由前端提供。因此,我们需要修改GetImageCode微服务的接口描述。
编辑back-end工程目录下,GetImageCode微服务目录中,proto子目录里的GetImageCode.proto脚本文件:
xxxxxxxxxx
61...
2message CallRequest {
3 // 图片验证码的UUID
4 string uuid = 1;
5}
6...
CallRequest表示前端提供给后端的请求数据,其中包含一个字符串型的uuid,即图片ID。
因为我们修改了接口描述文件,所以还要重新做一次编译,以使与之对应的Go语言代码文件,能够反映出我们所做的修改。
为此,我们在GetImageCode微服务目录中执行make命令,只携带一个目标参数,proto。
我们在前序课程中,对MVC架构曾做过详细的讲解。其中的M,即Model,代表模型层,通常指与数据库操作有关的代码。保存图片ID——图片验证码文本键值对的操作,显然属于模型层。
我们在GetImageCode微服务目录下,创建一个名为model的子目录,表示该微服务的模型层。在该子目录中创建redis.go文件,封装所有与操作Redis数据库有关的代码:
xxxxxxxxxx
171...
2// 将图片验证码的UUID和验证码字符串保存到Redis数据库中
3func SaveImageCode(uuid, str string) error {
4 // 连接数据库
5 conn, err := redis.Dial("tcp", "127.0.0.1:6379")
6 if err != nil {
7 return err
8 }
9
10 // 关闭数据库连接
11 defer conn.Close()
12
13 // 操作数据库
14 _, err = conn.Do("setex", uuid, 60*5, str)
15 return err
16}
17...
这里我们定义了一个名为SaveImageCode的函数,用于将图片验证码的UUID和验证码字符串保存到Redis数据库中。该函数带有两个输入参数,uuid为图片ID,str为图片验证码文本。首先调用Redigo的Dial函数,连接数据库,同时获得用于后续数据库操作的连接对象。然后通过连接对象的Do方法,将图片ID——图片验证码文本键值对添加到数据库中。注意这里使用的操作命令是“setex”,该命令可以指定超时,比如五分钟,超过五分钟数据库不响应即宣告失败。该函数返回Do方法返回的错误对象。
有了SaveImageCode函数,我们还需要在GetImageCode微服务的Call方法中调用该函数。
编辑back-end工程目录下,GetImageCode微服务目录中,handler子目录里的GetImageCode.go文件:
xxxxxxxxxx
331...
2// 获取图片验证码
3func (e *GetImageCode) Call(ctx context.Context, req *pb.CallRequest, rsp *pb.CallResponse) error {
4 logger.Infof("Received GetImageCode.Call request: %v", req)
5
6 // 生成图片验证码
7 cap := captcha.New()
8 cap.SetFont("./resources/comic.ttf")
9 cap.SetSize(128, 64)
10 cap.SetDisturbance(captcha.MEDIUM)
11 cap.SetFrontColor(color.RGBA{255, 255, 255, 255})
12 cap.SetBkgColor(color.RGBA{49, 174, 54, 255})
13 img, str := cap.Create(4, captcha.NUM)
14
15 // 序列化验证码图片
16 jsonImg, err := json.Marshal(img)
17 if err != nil {
18 logger.Error(err)
19 return err
20 }
21 rsp.Img = jsonImg
22
23 fmt.Println(str)
24
25 // 将图片验证码的UUID和验证码字符串保存到Redis数据库中
26 if err := model.SaveImageCode(req.Uuid, str); err != nil {
27 logger.Error(err)
28 return err
29 }
30
31 return nil
32}
33...
在上节课编写的Call方法的最后,添加对模型层SaveImageCode函数的调用,将从请求中获得的图片ID,和之前生成的图片验证码文本,作为参数传递给该函数,以键值对的形式保存到Redis数据库中。
请求中的图片ID,显然来自前端。因此我们还需要修改,前端服务器中调用后端远程方法的代码,在请求参数中增加图片ID。
因为我们已经修改了GetImageCode微服务的接口描述,这里需要将其同步到前端工程中。为此,我们将GetImageCode微服务目录下proto子目录中的所有文件,原封不动地复制到front-end工程目录proto子目录下的GetImageCode目录中。
编辑front-end工程目录下,controller子目录里的user.go文件:
xxxxxxxxxx
251...
2// 获取图片验证码
3func GetImageCode(ctx *gin.Context) {
4 // 获取图片验证码的UUID
5 uuid := ctx.Param("uuid")
6 fmt.Println(uuid)
7
8 // 调用微服务
9 srv := micro.NewService(micro.Registry(consul.NewRegistry()))
10 srv.Init()
11 c := pbGetImageCode.NewGetImageCodeService("getimagecode", srv.Client())
12 rsp, err := c.Call(context.Background(), &pbGetImageCode.CallRequest{
13 Uuid: uuid,
14 })
15 if err != nil {
16 fmt.Println(err)
17 return
18 }
19
20 // 反序列化验证码图片
21 var img captcha.Image
22 json.Unmarshal(rsp.Img, &img)
23 png.Encode(ctx.Writer, img)
24}
25...
很明显,在调用Call方法所提供的CallRequest参数中,增加了图片ID。
启动Consul服务器、GetImageCode微服务和前端服务器。从浏览器进入注册页面,在看到图片验证码的同时。
前端打印图片ID,后端打印图片验证码文本,Redis数据库中保存图片ID——图片验证码文本键值对。
谢谢大家,我们下节课再见!