使用 Go 和 Redis 的跟踪服务
让我们开始编码
让我们测试一下服务
想象一下,我们在像 Uber 这样的初创公司工作,需要创建一项新服务,该服务会定期保存司机的位置并进行处理。这样,当有人叫车时,我们就能找到距离我们取车点较近的司机。
这是我们服务的核心。保存位置并搜索附近的司机。这项服务我们使用 Go 和 Redis。
Redis
Redis 是一个开源(BSD 许可)的内存数据结构存储,可用作数据库、缓存和消息代理。它支持多种数据结构,例如字符串、哈希、列表、集合、支持范围查询的有序集合、位图、超日志以及支持半径查询的地理空间索引。Redis
Redis 具有多种功能,但为了提供此服务,我们将重点关注其地理空间功能。
首先我们需要安装 Redis,我建议使用 Docker 运行一个包含 Redis 的容器。只需按照这个命令,我们的机器上就会有一个运行 Redis 的容器。
docker run -d -p 6379:6379 redis
让我们开始编码
我们将为这项服务编写一个基本的实现,因为我想写一些关于如何改进这项服务的文章。我将以这段代码作为我下一篇文章的基础。
对于这项服务,我们需要使用为 Golang 提供 Redis 客户端的包“github.com/go-redis/redis”。
在你的工作目录中创建一个新的项目(文件夹)。我将其命名为“tracking”。首先,我们需要安装这个软件包。
go get -u github.com/go-redis/redis
然后我们创建文件“storages/redis.go”,其中包含帮助我们获取 Redis 客户端和一些与地理空间一起工作的功能的实现。
现在,我们创建一个包含指向 Redis 客户端指针的结构体。该指针将包含帮助我们实现此服务的函数。我们还将创建一个常量,其中包含 Redis 中集合的键名。
type RedisClient struct { *redis.Client }
const key = "drivers"
对于获取 Redis 客户端的功能,我们将在同步包及其 Once.Do 功能的帮助下使用单例模式。
在软件工程中,单例模式是一种软件设计模式,它将一个类的实例化限制为一个对象。当需要一个对象来协调整个系统的操作时,这种方法非常有用。如果您想了解更多关于单例模式的信息。
但是 Once.Do 是如何工作的呢?该结构体sync.Once
有一个原子计数器,atomic.StoreUint32
当函数被调用后,它会将值设置为 1,然后atomic.LoadUint32
判断是否需要再次调用。在这个基本实现中,GetRedisClient 将从两个端点调用,但我们只想获取一个实例。
var once sync.Once
var redisClient *RedisClient
func GetRedisClient() *RedisClient {
once.Do(func() {
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
redisClient = &RedisClient{client}
})
_, err := redisClient.Ping().Result()
if err != nil {
log.Fatalf("Could not connect to redis %v", err)
}
return redisClient
}
然后我们为RedisClient创建三个函数。
AddDriverLocation:将指定的地理空间项(纬度、经度、名称,在本例中,名称是司机 ID)添加到指定的键。你还记得我们一开始在 Redis 中为 Set 定义的键吗?就是这个。
func (c *RedisClient) AddDriverLocation(lng, lat float64, id string) {
c.GeoAdd(
key,
&redis.GeoLocation{Longitude: lng, Latitude: lat, Name: id},
)
}
RemoveDriverLocation:客户端 redis 没有 GeoDel 函数,因为没有 GEODEL 命令,所以我们可以使用 ZREM 来删除元素。Geo 索引结构只是一个有序集合。
func (c *RedisClient) RemoveDriverLocation(id string) {
c.ZRem(key, id)
}
SearchDrivers:GeoRadius 函数实现了 GEORADIUS 命令,该命令使用 GEOADD 返回已填充地理空间信息的有序集合的成员,这些成员位于指定中心位置和与中心的最大距离(半径)的区域边界内。如果您想了解更多信息,请访问GEORADIUS。
func (c *RedisClient) SearchDrivers(limit int, lat, lng, r float64) []redis.GeoLocation {
/*
WITHDIST: Also return the distance of the returned items from the
specified center. The distance is returned in the same unit as the unit
specified as the radius argument of the command.
WITHCOORD: Also return the longitude,latitude coordinates of the matching items.
WITHHASH: Also return the raw geohash-encoded sorted set score of the item,
in the form of a 52 bit unsigned integer. This is only useful for low level
hacks or debugging and is otherwise of little interest for the general user.
*/
res, _ := c.GeoRadius(key, lng, lat, &redis.GeoRadiusQuery{
Radius: r,
Unit: "km",
WithGeoHash: true,
WithCoord: true,
WithDist: true,
Count: limit,
Sort: "ASC",
}).Result()
return res
}
接下来,创建一个main.go
package main
import (
"net/http"
"fmt"
"log"
)
func main() {
// We create a simple httpserver
server := http.Server{
Addr: fmt.Sprint(":8000"),
Handler: NewHandler(),
}
// Run server
log.Printf("Starting HTTP Server. Listening at %q", server.Addr)
if err := server.ListenAndServe(); err != nil {
log.Printf("%v", err)
} else {
log.Println("Server closed ! ")
}
}
我们使用 http.Server 创建一个简单的服务器。
然后我们创建包含应用程序端点的文件“handler/handler.go”。
func NewHandler() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("tracking", tracking)
mux.HandleFunc("search", search)
return mux
}
我们使用 http.ServeMux 来处理我们的端点,我们为我们的服务创建两个端点。
第一个“跟踪”端点用于保存司机发送的最后一个位置信息,在本例中,我们只想保存最后一个位置信息。我们可以修改此端点,以便将之前的位置信息保存在另一个数据库中。
func tracking(w http.ResponseWriter, r *http.Request) {
// crate an anonymous struct for driver data.
var driver = struct {
ID string `json:"id"`
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
}{}
rClient := storages.GetRedisClient()
if err := json.NewDecoder(r.Body).Decode(&driver); err != nil {
log.Printf("could not decode request: %v", err)
http.Error(w, "could not decode request", http.StatusInternalServerError)
return
}
// Add new location
// You can save locations in another db
rClient.AddDriverLocation(driver.Lng, driver.Lat, driver.ID)
w.WriteHeader(http.StatusOK)
return
}
第二个端点是“搜索”,通过这个端点,我们可以找到给定点附近的所有驱动程序,
// search receives lat and lng of the picking point and searches drivers about this point.
func search(w http.ResponseWriter, r *http.Request) {
rClient := storages.GetRedisClient()
body := struct {
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
Limit int `json:"limit"`
}{}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
log.Printf("could not decode request: %v", err)
http.Error(w, "could not decode request", http.StatusInternalServerError)
return
}
drivers := rClient.SearchDrivers(body.Limit, body.Lat, body.Lng, 15)
data, err := json.Marshal(drivers)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(data)
return
}
让我们测试一下服务
首先,运行服务器。
go run main.go
接下来,我们需要添加四个驱动程序位置。
我们在地图上添加了四名司机,如上图所示,绿色线条表示接送点和司机之间的距离。
curl -i --header "Content-Type: application/json" --data '{"id": "1", "lat": -33.44091, "lng": -70.6301}' http://localhost:8000/tracking
curl -i --header "Content-Type: application/json" --data '{"id": "2", "lat": -33.44005, "lng": -70.63279}' http://localhost:8000/tracking
curl -i --header "Content-Type: application/json" --data '{"id": "3", "lat": -33.44338, "lng": -70.63335}' http://localhost:8000/tracking
curl -i --header "Content-Type: application/json" --data '{"id": "4", "lat": -33.44186, "lng": -70.62653}' http://localhost:8000/tracking
由于我们现在有了司机的位置,我们可以进行空间搜索。
我们将寻找 4 位附近的司机
curl -i --header "Content-Type: application/json" --data '{"lat": -33.44262, "lng": -70.63054, "limit": 5}' http://localhost:8000/search
正如您所看到的,结果与地图相匹配,请参阅地图中的绿色线条。
HTTP/1.1 200 OK
Content-Type: application/json
Date: Wed, 08 Aug 2018 05:07:57 GMT
Content-Length: 456
[
{
"Name": "1",
"Longitude": -70.63009768724442,
"Latitude": -33.44090957099124,
"Dist": 0.1946,
"GeoHash": 861185092131738
},
{
"Name": "3",
"Longitude": -70.63334852457047,
"Latitude": -33.44338092412159,
"Dist": 0.2741,
"GeoHash": 861185074815667
},
{
"Name": "2",
"Longitude": -70.63279062509537,
"Latitude": -33.44005030051822,
"Dist": 0.354,
"GeoHash": 861185086448695
},
{
"Name": "4",
"Longitude": -70.62653034925461,
"Latitude": -33.44186009142599,
"Dist": 0.3816,
"GeoHash": 861185081504625
}
]
查找最近的司机
curl -i --header "Content-Type: application/json" --data '{"lat": -33.44262, "lng": -70.63054, "limit": 1}' http://localhost:8000/search
结果
HTTP/1.1 200 OK
Content-Type: application/json
Date: Wed, 08 Aug 2018 05:12:24 GMT
Content-Length: 115
[{"Name":"1","Longitude":-70.63009768724442,"Latitude":-33.44090957099124,"Dist":0.1946,"GeoHash":861185092131738}]