UDN-企业互联网技术人气社区

板块导航

浏览  : 721
回复  : 0

[讨论交流] 如何让Redis与Go密切配合

[复制链接]
巡山霉少女的头像 楼主
发表于 2016-3-13 16:57:30 | 显示全部楼层 |阅读模式

  这篇blog将给大家介绍在go语言项目使用Redis作为数据持久层。开始之前我将介绍几个基本概念,然后构建一个go web项目来展示如何让Redis实现安全的并发。

  这篇blog是假设你是了解Redis的一些东西的,如果你之前没有学习或者了解过Redis。我建议你先去阅读一些Redis的知识再来看这篇blog效果更好。

  安装驱动

  第一步我们需要安装Redis的go语言驱动程序,大家可以在这个列表中选择一个http://redis.io/clients#go

  这篇blog的示例我还是选择了Radix.v2作为驱动程序。因为它维护的很好,API用起来也很舒服,如果你也想用的话 可以在终端执行下面的命令:

  1. $ go get github.com/mediocregopher/radix.v2
复制代码


  有一点要注意的是Radix.v2的包被分为了(cluster, pool, pubsub,redis, sentinel and util)6个子包。最开始我们只需要使用其中的redis包。

  1. import (
  2.     "github.com/mediocregopher/radix.v2/redis"
  3. )
复制代码


  开始使用 Radix.v2

  作为示例程序,我们为它设定一个场景也就是所谓的需求。假设我们有一个在线的唱片店,并希望存储有关的专辑的销售信息存储在Redis中。我们可以有很多不同的方式来设计数据模式。我希望我们的model是简单的并且通过hash的方式来存储我们的专辑信息包括标题,艺术家和价格之类的字段。

  我们可以通过Redis的CLI  执行HMSET命令:

  1. 127.0.0.1:6379> HMSET album:1 title "Electric Ladyland" artist "Jimi Hendrix" price 4.95 likes 8
  2. OK
复制代码


  如果我们在Go项目中来做同样的操作,我们就需要通过Radix.v2 redis包来实现,首先需要Dial()函数,需要它返回一个新的connection。第二个我们需要使用client.Cmd()方法。通过他我们可以发送一个命令到我们的服务器,这里会返回给我们一个Resp对象的指针。

  我们来看一个简单的示例:

  1. File: main.go
  2. package main

  3. import (
  4.     "fmt"
  5.     // Import the Radix.v2 redis package.
  6.     "github.com/mediocregopher/radix.v2/redis"
  7.     "log"
  8. )

  9. func main() {
  10.     // Establish a connection to the Redis server listening on port 6379 of the
  11.     // local machine. 6379 is the default port, so unless you've already
  12.     // changed the Redis configuration file this should work.
  13.     conn, err := redis.Dial("tcp", "localhost:6379")
  14.     if err != nil {
  15.         log.Fatal(err)
  16.     }
  17.     // Importantly, use defer to ensure the connection is always properly
  18.     // closed before exiting the main() function.
  19.     defer conn.Close()

  20.     // Send our command across the connection. The first parameter to Cmd()
  21.     // is always the name of the Redis command (in this example HMSET),
  22.     // optionally followed by any necessary arguments (in this example the
  23.     // key, followed by the various hash fields and values).
  24.     resp := conn.Cmd("HMSET", "album:1", "title", "Electric Ladyland", "artist", "Jimi Hendrix", "price", 4.95, "likes", 8)
  25.     // Check the Err field of the *Resp object for any errors.
  26.     if resp.Err != nil {
  27.         log.Fatal(resp.Err)
  28.     }

  29.     fmt.Println("Electric Ladyland added!")
  30. }
复制代码


  在这个示例中我们所关心的并不是Redis会返回什么。因为所有成功的操作都会返回一个“OK”的字符串。所以我们不需要对*Resp对象做错误检查。

  在这样的情况下我们只需要检查err就好了:

  1. err = conn.Cmd("HMSET", "album:1", "title", "Electric Ladyland", "artist", "Jimi Hendrix", "price", 4.95, "likes", 8).Err
  2. if err != nil {
  3.     log.Fatal(err)
  4. }
复制代码


  回复处理

  如果我们留意Redis的回复的话,我们发现Resp对象有很多有用的函数让Go的类型转换变得容易:

  1. Resp.Bytes() – converts a single reply to a byte slice ([]byte)
  2. Resp.Float64() – converts a single reply to a Float64
  3. Resp.Int() – converts a single reply to a int
  4. Resp.Int64() – converts a single reply to a int64
  5. Resp.Str() – converts a single reply to a string
  6. Resp.Array() – converts an array reply to an slice of individual Resp objects ([]*Resp)
  7. Resp.List() – converts an array reply to an slice of strings ([]string)
  8. Resp.ListBytes() – converts an array reply to an slice of byte slices ([][]byte)
  9. Resp.Map() – converts an array reply to a map of strings, using each item in the array reply alternately as the keys and values for the map (map[string]string)
复制代码


  接下来我们使用HGET 命令获取一些专辑的数据:

  1. package main

  2. import (
  3.     "fmt"
  4.     "github.com/mediocregopher/radix.v2/redis"
  5.     "log"
  6. )

  7. func main() {
  8.     conn, err := redis.Dial("tcp", "localhost:6379")
  9.     if err != nil {
  10.         log.Fatal(err)
  11.     }
  12.     defer conn.Close()

  13.     // Issue a HGET command to retrieve the title for a specific album, and use
  14.     // the Str() helper method to convert the reply to a string.
  15.     title, err := conn.Cmd("HGET", "album:1", "title").Str()
  16.     if err != nil {
  17.         log.Fatal(err)
  18.     }

  19.     // Similarly, get the artist and convert it to a string.
  20.     artist, err := conn.Cmd("HGET", "album:1", "artist").Str()
  21.     if err != nil {
  22.         log.Fatal(err)
  23.     }

  24.     // And the price as a float64...
  25.     price, err := conn.Cmd("HGET", "album:1", "price").Float64()
  26.     if err != nil {
  27.         log.Fatal(err)
  28.     }

  29.     // And the number of likes as an integer.
  30.     likes, err := conn.Cmd("HGET", "album:1", "likes").Int()
  31.     if err != nil {
  32.         log.Fatal(err)
  33.     }

  34.     fmt.Printf("%s by %s: £%.2f [%d likes]\n", title, artist, price, likes)
  35. }
复制代码


  值得指出的是,当我们使用这些方法的时候,这时候的错误返回会设置一种或者2种错误:一种是任一命令执行失败,或者返回数据转换成了我们需要要的类型。这样的话我们就不知道错误是哪一种了?除非我们检查错误消息的方式。

  如果我们执行代码我们将会看到:

  1. $ go run main.go
  2. Electric Ladyland by Jimi Hendrix: £4.95 [8 likes]
复制代码


  现在让我们来看一个更完整的例子,我们使用HGETALL命令来获取专辑的信息:

  1. File: main.go
  2. package main

  3. import (
  4.     "fmt"
  5.     "github.com/mediocregopher/radix.v2/redis"
  6.     "log"
  7.     "strconv"
  8. )

  9. // Define a custom struct to hold Album data.
  10. type Album struct {
  11.     Title  string
  12.     Artist string
  13.     Price  float64
  14.     Likes  int
  15. }

  16. func main() {
  17.     conn, err := redis.Dial("tcp", "localhost:6379")
  18.     if err != nil {
  19.         log.Fatal(err)
  20.     }
  21.     defer conn.Close()

  22.     // Fetch all album fields with the HGETALL command. Because HGETALL
  23.     // returns an array reply, and because the underlying data structure in
  24.     // Redis is a hash, it makes sense to use the Map() helper function to
  25.     // convert the reply to a map[string]string.
  26.     reply, err := conn.Cmd("HGETALL", "album:1").Map()
  27.     if err != nil {
  28.         log.Fatal(err)
  29.     }

  30.     // Use the populateAlbum helper function to create a new Album object from
  31.     // the map[string]string.
  32.     ab, err := populateAlbum(reply)
  33.     if err != nil {
  34.         log.Fatal(err)
  35.     }

  36.     fmt.Println(ab)
  37. }

  38. // Create, populate and return a pointer to a new Album struct, based on data
  39. // from a map[string]string.
  40. func populateAlbum(reply map[string]string) (*Album, error) {
  41.     var err error
  42.     ab := new(Album)
  43.     ab.Title = reply["title"]
  44.     ab.Artist = reply["artist"]
  45.     // We need to use the strconv package to convert the 'price' value from a
  46.     // string to a float64 before assigning it.
  47.     ab.Price, err = strconv.ParseFloat(reply["price"], 64)
  48.     if err != nil {
  49.         return nil, err
  50.     }
  51.     // Similarly, we need to convert the 'likes' value from a string to an
  52.     // integer.
  53.     ab.Likes, err = strconv.Atoi(reply["likes"])
  54.     if err != nil {
  55.         return nil, err
  56.     }
  57.     return ab, nil
  58. }
复制代码


  我们执行代码将会看到下面的输出:

  1. $ go run main.go
  2. &{Electric Ladyland Jimi Hendrix 4.95 8}
复制代码


  在Web项目中使用

  我们知道什么是并发不安全的使用Radix.v2中的redis包是很重要。

  如果我们通过多个goroutines访问一个Redis服务器,那我们在web项目中就需要使用pool包来替换redis。这将是我们能够建立连接池,每次使用连接我们只需要从连接池中获取就可以了。

  执行命令,然后返回到连接池中。

  简单的介绍下我们的Web项目的结构:

  1. Method        Path        Function
  2. GET        /album?id=1        Show details of a specific album (using the id provided
  3. in the query string)
  4. POST        /like        Add a new like for a specific album (using the id
  5. provided in the request body)
  6. GET        /popular        List the top 3 most liked albums in order
复制代码


  如果你想和我的结构一样的话你可以跟着我下面的命令操作:

  1. $ cd $GOPATH/src
  2. $ mkdir -p recordstore/models
  3. $ cd recordstore
  4. $ touch main.go models/albums.go
  5. $ tree
  6. .
  7. ├── main.go
  8. └── models
  9.     └── albums.go
复制代码


  然后我们通过Redis CLI 保存一些专辑数据,这样我们等下就有测试数据了。

  1. HMSET album:1 title "Electric Ladyland" artist "Jimi Hendrix" price 4.95 likes 8
  2. HMSET album:2 title "Back in Black" artist "AC/DC" price 5.95 likes 3
  3. HMSET album:3 title "Rumours" artist "Fleetwood Mac" price 7.95 likes 12
  4. HMSET album:4 title "Nevermind" artist "Nirvana" price 5.95 likes 8
  5. ZADD likes 8 1 3 2 12 3 8 4
复制代码


  我们将遵循MVC的模式来构建我们的项目,我们使用models/albums.go文件来实现我们Redis的逻辑处理,在models/albums.go文件中我们将使用init()函数来建立启动我们的Redis连接池。然后我们也会重构之前我们写的FindAlbum()函数。然后我们我们通过HTTP handlers来使用它:

  1. File: models/albums.go
  2. package models

  3. import (
  4.     "errors"
  5.     // Import the Radix.v2 pool package, NOT the redis package.
  6.     "github.com/mediocregopher/radix.v2/pool"
  7.     "log"
  8.     "strconv"
  9. )

  10. // Declare a global db variable to store the Redis connection pool.
  11. var db *pool.Pool

  12. func init() {
  13.     var err error
  14.     // Establish a pool of 10 connections to the Redis server listening on
  15.     // port 6379 of the local machine.
  16.     db, err = pool.New("tcp", "localhost:6379", 10)
  17.     if err != nil {
  18.         log.Panic(err)
  19.     }
  20. }

  21. // Create a new error message and store it as a constant. We'll use this
  22. // error later if the FindAlbum() function fails to find an album with a
  23. // specific id.
  24. var ErrNoAlbum = errors.New("models: no album found")

  25. type Album struct {
  26.     Title  string
  27.     Artist string
  28.     Price  float64
  29.     Likes  int
  30. }

  31. func populateAlbum(reply map[string]string) (*Album, error) {
  32.     var err error
  33.     ab := new(Album)
  34.     ab.Title = reply["title"]
  35.     ab.Artist = reply["artist"]
  36.     ab.Price, err = strconv.ParseFloat(reply["price"], 64)
  37.     if err != nil {
  38.         return nil, err
  39.     }
  40.     ab.Likes, err = strconv.Atoi(reply["likes"])
  41.     if err != nil {
  42.         return nil, err
  43.     }
  44.     return ab, nil
  45. }

  46. func FindAlbum(id string) (*Album, error) {
  47.     // Use the connection pool's Get() method to fetch a single Redis
  48.     // connection from the pool.
  49.     conn, err := db.Get()
  50.     if err != nil {
  51.         return nil, err
  52.     }
  53.     // Importantly, use defer and the connection pool's Put() method to ensure
  54.     // that the connection is always put back in the pool before FindAlbum()
  55.     // exits.
  56.     defer db.Put(conn)

  57.     // Fetch the details of a specific album. If no album is found with the
  58.     // given id, the map[string]string returned by the Map() helper method
  59.     // will be empty. So we can simply check whether it's length is zero and
  60.     // return an ErrNoAlbum message if necessary.
  61.     reply, err := conn.Cmd("HGETALL", "album:"+id).Map()
  62.     if err != nil {
  63.         return nil, err
  64.     } else if len(reply) == 0 {
  65.         return nil, ErrNoAlbum
  66.     }

  67.     return populateAlbum(reply)
  68. }
复制代码


  值得我们着重讲解一下的是pool.New()函数。在上面的代码中我们指定连接池的大小为10,它只是空闲时候在池中等待的数量。如果10个连接都使用了的话,那时候将会调用 pool.Get()创建新的链接。

  当你只有一个连接发出命令的话,比如上面的FindAlbum()函数,有可能会使用pool.Cmd()的快捷方式。这将自动从连接池中获取一个新的连接,执行给定的命令、然后将连接返回连接池。

  这是我们重构了之后的FindAlbum()函数:

  1. func FindAlbum(id string) (*Album, error) {
  2.     reply, err := db.Cmd("HGETALL", "album:"+id).Map()
  3.     if err != nil {
  4.         return nil, err
  5.     } else if len(reply) == 0 {
  6.         return nil, ErrNoAlbum
  7.     }

  8.     return populateAlbum(reply)
  9. }
复制代码


  好了,让我们来看看我们的main.go的文件如何实现的:

  1. File: main.go
  2. package main

  3. import (
  4.     "fmt"
  5.     "net/http"
  6.     "recordstore/models"
  7.     "strconv"
  8. )

  9. func main() {
  10.     // Use the showAlbum handler for all requests with a URL path beginning
  11.     // '/album'.
  12.     http.HandleFunc("/album", showAlbum)
  13.     http.ListenAndServe(":3000", nil)
  14. }

  15. func showAlbum(w http.ResponseWriter, r *http.Request) {
  16.     // Unless the request is using the GET method, return a 405 'Method Not
  17.     // Allowed' response.
  18.     if r.Method != "GET" {
  19.         w.Header().Set("Allow", "GET")
  20.         http.Error(w, http.StatusText(405), 405)
  21.         return
  22.     }

  23.     // Retrieve the id from the request URL query string. If there is no id
  24.     // key in the query string then Get() will return an empty string. We
  25.     // check for this, returning a 400 Bad Request response if it's missing.
  26.     id := r.URL.Query().Get("id")
  27.     if id == "" {
  28.         http.Error(w, http.StatusText(400), 400)
  29.         return
  30.     }
  31.     // Validate that the id is a valid integer by trying to convert it,
  32.     // returning a 400 Bad Request response if the conversion fails.
  33.     if _, err := strconv.Atoi(id); err != nil {
  34.         http.Error(w, http.StatusText(400), 400)
  35.         return
  36.     }

  37.     // Call the FindAlbum() function passing in the user-provided id. If
  38.     // there's no matching album found, return a 404 Not Found response. In
  39.     // the event of any other errors, return a 500 Internal Server Error
  40.     // response.
  41.     bk, err := models.FindAlbum(id)
  42.     if err == models.ErrNoAlbum {
  43.         http.NotFound(w, r)
  44.         return
  45.     } else if err != nil {
  46.         http.Error(w, http.StatusText(500), 500)
  47.         return
  48.     }

  49.     // Write the album details as plain text to the client.
  50.     fmt.Fprintf(w, "%s by %s: £%.2f [%d likes] \n", bk.Title, bk.Artist, bk.Price, bk.Likes)
  51. }
复制代码

  
  执行我们的代码:

  1. $ go run main.go
  2. 1
  3. $ go run main.go
复制代码


  通过cURL来测试我们的请求,我们也可以通过postman之类的工具:

  1. $ curl -i localhost:3000/album?id=2
  2. HTTP/1.1 200 OK
  3. Content-Length: 42
  4. Content-Type: text/plain; charset=utf-8

  5. Back in Black by AC/DC: £5.95 [3 likes]
复制代码


  使用事务

  我们的第二个路由POST /likes

  当我们的用户如果喜欢我们的专辑的时候,我们需要执行2条不同的命令:使用HINCRBY来增加likes字段的值,和使用ZINCRBY来增加相应的score在我们的likes的有序集合中。

  那么这里就会产生一个问题。理想情况下,我们希望这两个键在完全相同的时间递增作为一个原子操作,当一个key完成更新后,其它数据不会与其发生争抢。

  解决这个问题的办法就需要我们使用Redis事务,这让我们运行多个命令在一起作为一个原子团。我们可以使用MULTI命令来开启事务,随后执行我们的HINCRBY 和ZINCRBY,最后执行EXEC命令

  让我们在model中创建一个新的IncrementLikes()函数:

  1. File: models/albums.go
  2. ...
  3. func IncrementLikes(id string) error {
  4.     conn, err := db.Get()
  5.     if err != nil {
  6.         return err
  7.     }
  8.     defer db.Put(conn)

  9.     // Before we do anything else, check that an album with the given id
  10.     // exists. The EXISTS command returns 1 if a specific key exists
  11.     // in the database, and 0 if it doesn't.
  12.     exists, err := conn.Cmd("EXISTS", "album:"+id).Int()
  13.     if err != nil {
  14.         return err
  15.     } else if exists == 0 {
  16.         return ErrNoAlbum
  17.     }

  18.     // Use the MULTI command to inform Redis that we are starting a new
  19.     // transaction.
  20.     err = conn.Cmd("MULTI").Err
  21.     if err != nil {
  22.         return err
  23.     }

  24.     // Increment the number of likes in the album hash by 1. Because it
  25.     // follows a MULTI command, this HINCRBY command is NOT executed but
  26.     // it is QUEUED as part of the transaction. We still need to check
  27.     // the reply's Err field at this point in case there was a problem
  28.     // queueing the command.
  29.     err = conn.Cmd("HINCRBY", "album:"+id, "likes", 1).Err
  30.     if err != nil {
  31.         return err
  32.     }
  33.     // And we do the same with the increment on our sorted set.
  34.     err = conn.Cmd("ZINCRBY", "likes", 1, id).Err
  35.     if err != nil {
  36.         return err
  37.     }

  38.     // Execute both commands in our transaction together as an atomic group.
  39.     // EXEC returns the replies from both commands as an array reply but,
  40.     // because we're not interested in either reply in this example, it
  41.     // suffices to simply check the reply's Err field for any errors.
  42.     err = conn.Cmd("EXEC").Err
  43.     if err != nil {
  44.         return err
  45.     }
  46.     return nil
  47. }
复制代码


  我们也需要在main.go文件中为route添加一个addLike()handler:


  1. File: main.go
  2. func main() {
  3.     http.HandleFunc("/album", showAlbum)
  4.     http.HandleFunc("/like", addLike)
  5.     http.ListenAndServe(":3000", nil)
  6. }
  7. ...
  8. func addLike(w http.ResponseWriter, r *http.Request) {
  9.     // Unless the request is using the POST method, return a 405 'Method Not
  10.     // Allowed' response.
  11.     if r.Method != "POST" {
  12.         w.Header().Set("Allow", "POST")
  13.         http.Error(w, http.StatusText(405), 405)
  14.         return
  15.     }

  16.     // Retreive the id from the POST request body. If there is no parameter
  17.     // named "id" in the request body then PostFormValue() will return an
  18.     // empty string. We check for this, returning a 400 Bad Request response
  19.     // if it's missing.
  20.     id := r.PostFormValue("id")
  21.     if id == "" {
  22.         http.Error(w, http.StatusText(400), 400)
  23.         return
  24.     }
  25.     // Validate that the id is a valid integer by trying to convert it,
  26.     // returning a 400 Bad Request response if the conversion fails.
  27.     if _, err := strconv.Atoi(id); err != nil {
  28.         http.Error(w, http.StatusText(400), 400)
  29.         return
  30.     }

  31.     // Call the IncrementLikes() function passing in the user-provided id. If
  32.     // there's no album found with that id, return a 404 Not Found response.
  33.     // In the event of any other errors, return a 500 Internal Server Error
  34.     // response.
  35.     err := models.IncrementLikes(id)
  36.     if err == models.ErrNoAlbum {
  37.         http.NotFound(w, r)
  38.         return
  39.     } else if err != nil {
  40.         http.Error(w, http.StatusText(500), 500)
  41.         return
  42.     }

  43.     // Redirect the client to the GET /ablum route, so they can see the
  44.     // impact their like has had.
  45.     http.Redirect(w, r, "/album?id="+id, 303)
  46. }
复制代码


  和之前一样我们来测试一下:

  1. $ curl -i -L -d "id=2" localhost:3000/like
  2. HTTP/1.1 303 See Other
  3. Location: /album?id=2
  4. Date: Thu, 25 Feb 2016 17:08:19 GMT
  5. Content-Length: 0
  6. Content-Type: text/plain; charset=utf-8

  7. HTTP/1.1 200 OK
  8. Content-Length: 42
  9. Content-Type: text/plain; charset=utf-8

  10. Back in Black by AC/DC: £5.95 [4 likes]
复制代码


  使用Watch命令

  好了还剩下我们最后一个route: GET /popular,这个route将会现实我们最受欢迎的3个专辑的详细内容,我们将在models/albums.go中创建FindTopThree()函数,这这个函数中我们需要:

  1.使用ZREVRANGE命令,去获取3条专家的最高分,从我们的likes字段的有序集合中

  2.通过HGETALL命令循环获取存储在键的散列的所有字段和值,把它们存到[]*Album slice中

  这里同样也可能会发送数据竞争的情况,如果第二个第二个客户端也碰巧需要只想我们刚刚的操作,我们用户可能会得到不正确的数据。

  这个问题的解决方法就是使用Redis WATCH命令与事务结合使用。WATCH可以让Redis监控某个key的变化,如果其它客户端要在我们执行EXEC之前更改我们监控的key,那么事务将会失败并且会返回Nil。如果没有其它客户端在我们执行EXEC命令之前修改我们的数据。那么我的操作将正常执行:


  1. File: models/albums.go
  2. package models

  3. import (
  4.     "errors"
  5.     "github.com/mediocregopher/radix.v2/pool"
  6.     // Import the Radix.v2 redis package (we need access to its Nil type).
  7.     "github.com/mediocregopher/radix.v2/redis"
  8.     "log"
  9.     "strconv"
  10. )
  11. ...
  12. func FindTopThree() ([]*Album, error) {
  13.     conn, err := db.Get()
  14.     if err != nil {
  15.         return nil, err
  16.     }
  17.     defer db.Put(conn)

  18.     // Begin an infinite loop.
  19.     for {
  20.         // Instruct Redis to watch the likes sorted set for any changes.
  21.         err = conn.Cmd("WATCH", "likes").Err
  22.         if err != nil {
  23.             return nil, err
  24.         }

  25.         // Use the ZREVRANGE command to fetch the album ids with the highest
  26.         // score (i.e. most likes) from our 'likes' sorted set. The ZREVRANGE
  27.         // start and stop values are zero-based indexes, so we use 0 and 2
  28.         // respectively to limit the reply to the top three. Because ZREVRANGE
  29.         // returns an array response, we use the List() helper function to
  30.         // convert the reply into a []string.
  31.         reply, err := conn.Cmd("ZREVRANGE", "likes", 0, 2).List()
  32.         if err != nil {
  33.             return nil, err
  34.         }

  35.         // Use the MULTI command to inform Redis that we are starting a new
  36.         // transaction.
  37.         err = conn.Cmd("MULTI").Err
  38.         if err != nil {
  39.             return nil, err
  40.         }

  41.         // Loop through the ids returned by ZREVRANGE, queuing HGETALL
  42.         // commands to fetch the individual album details.
  43.         for _, id := range reply {
  44.             err := conn.Cmd("HGETALL", "album:"+id).Err
  45.             if err != nil {
  46.                 return nil, err
  47.             }
  48.         }

  49.         // Execute the transaction. Importantly, use the Resp.IsType() method
  50.         // to check whether the reply from EXEC was nil or not. If it is nil
  51.         // it means that another client changed the WATCHed likes sorted set,
  52.         // so we use the continue command to re-run the loop.
  53.         ereply := conn.Cmd("EXEC")
  54.         if ereply.Err != nil {
  55.             return nil, err
  56.         } else if ereply.IsType(redis.Nil) {
  57.             continue
  58.         }

  59.         // Otherwise, use the Array() helper function to convert the
  60.         // transaction reply to an array of Resp objects ([]*Resp).
  61.         areply, err := ereply.Array()
  62.         if err != nil {
  63.             return nil, err
  64.         }

  65.         // Create a new slice to store the album details.
  66.         abs := make([]*Album, 3)

  67.         // Iterate through the array of Resp objects, using the Map() helper
  68.         // to convert the individual reply into a map[string]string, and then
  69.         // the populateAlbum function to create a new Album object
  70.         // from the map. Finally store them in order in the abs slice.
  71.         for i, reply := range areply {
  72.             mreply, err := reply.Map()
  73.             if err != nil {
  74.                 return nil, err
  75.             }
  76.             ab, err := populateAlbum(mreply)
  77.             if err != nil {
  78.                 return nil, err
  79.             }
  80.             abs[i] = ab
  81.         }

  82.         return abs, nil
  83.     }
  84. }
复制代码

  1. File: models/albums.go
  2. package models

  3. import (
  4.     "errors"
  5.     "github.com/mediocregopher/radix.v2/pool"
  6.     // Import the Radix.v2 redis package (we need access to its Nil type).
  7.     "github.com/mediocregopher/radix.v2/redis"
  8.     "log"
  9.     "strconv"
  10. )
  11. ...
  12. func FindTopThree() ([]*Album, error) {
  13.     conn, err := db.Get()
  14.     if err != nil {
  15.         return nil, err
  16.     }
  17.     defer db.Put(conn)

  18.     // Begin an infinite loop.
  19.     for {
  20.         // Instruct Redis to watch the likes sorted set for any changes.
  21.         err = conn.Cmd("WATCH", "likes").Err
  22.         if err != nil {
  23.             return nil, err
  24.         }

  25.         // Use the ZREVRANGE command to fetch the album ids with the highest
  26.         // score (i.e. most likes) from our 'likes' sorted set. The ZREVRANGE
  27.         // start and stop values are zero-based indexes, so we use 0 and 2
  28.         // respectively to limit the reply to the top three. Because ZREVRANGE
  29.         // returns an array response, we use the List() helper function to
  30.         // convert the reply into a []string.
  31.         reply, err := conn.Cmd("ZREVRANGE", "likes", 0, 2).List()
  32.         if err != nil {
  33.             return nil, err
  34.         }

  35.         // Use the MULTI command to inform Redis that we are starting a new
  36.         // transaction.
  37.         err = conn.Cmd("MULTI").Err
  38.         if err != nil {
  39.             return nil, err
  40.         }

  41.         // Loop through the ids returned by ZREVRANGE, queuing HGETALL
  42.         // commands to fetch the individual album details.
  43.         for _, id := range reply {
  44.             err := conn.Cmd("HGETALL", "album:"+id).Err
  45.             if err != nil {
  46.                 return nil, err
  47.             }
  48.         }

  49.         // Execute the transaction. Importantly, use the Resp.IsType() method
  50.         // to check whether the reply from EXEC was nil or not. If it is nil
  51.         // it means that another client changed the WATCHed likes sorted set,
  52.         // so we use the continue command to re-run the loop.
  53.         ereply := conn.Cmd("EXEC")
  54.         if ereply.Err != nil {
  55.             return nil, err
  56.         } else if ereply.IsType(redis.Nil) {
  57.             continue
  58.         }

  59.         // Otherwise, use the Array() helper function to convert the
  60.         // transaction reply to an array of Resp objects ([]*Resp).
  61.         areply, err := ereply.Array()
  62.         if err != nil {
  63.             return nil, err
  64.         }

  65.         // Create a new slice to store the album details.
  66.         abs := make([]*Album, 3)

  67.         // Iterate through the array of Resp objects, using the Map() helper
  68.         // to convert the individual reply into a map[string]string, and then
  69.         // the populateAlbum function to create a new Album object
  70.         // from the map. Finally store them in order in the abs slice.
  71.         for i, reply := range areply {
  72.             mreply, err := reply.Map()
  73.             if err != nil {
  74.                 return nil, err
  75.             }
  76.             ab, err := populateAlbum(mreply)
  77.             if err != nil {
  78.                 return nil, err
  79.             }
  80.             abs[i] = ab
  81.         }

  82.         return abs, nil
  83.     }
  84. }
复制代码


  和之前一样在route中添加我们的handle:

  1. File: main.go
  2. func main() {
  3.     http.HandleFunc("/album", showAlbum)
  4.     http.HandleFunc("/like", addLike)
  5.     http.HandleFunc("/popular", listPopular)
  6.     http.ListenAndServe(":3000", nil)
  7. }
  8. ...
  9. func listPopular(w http.ResponseWriter, r *http.Request) {
  10.   // Unless the request is using the GET method, return a 405 'Method Not
  11.   // Allowed' response.
  12.   if r.Method != "GET" {
  13.     w.Header().Set("Allow", "GET")
  14.     http.Error(w, http.StatusText(405), 405)
  15.     return
  16.   }

  17.   // Call the FindTopThree() function, returning a return a 500 Internal
  18.   // Server Error response if there's any error.
  19.   abs, err := models.FindTopThree()
  20.   if err != nil {
  21.     http.Error(w, http.StatusText(500), 500)
  22.     return
  23.   }

  24.   // Loop through the 3 albums, writing the details as a plain text list
  25.   // to the client.
  26.   for i, ab := range abs {
  27.     fmt.Fprintf(w, "%d) %s by %s: £%.2f [%d likes] \n", i+1, ab.Title, ab.Artist, ab.Price, ab.Likes)
  28.   }
  29. }
复制代码


  通过curl 发送GET /popular route请求,我们将看到:

  1. $ curl -i localhost:3000/popular
  2. HTTP/1.1 200 OK
  3. Content-Length: 147
  4. Content-Type: text/plain; charset=utf-8

  5. 1) Rumours by Fleetwood Mac: £7.95 [12 likes]
  6. 2) Nevermind by Nirvana: £5.95 [8 likes]
  7. 3) Electric Ladyland by Jimi Hendrix: £4.95 [8 likes]
复制代码

相关帖子

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部