Merge pull request #25 from noahcampbell/master

Internal API changes and introduce testing.
This commit is contained in:
Steve Francia 2013-08-09 18:01:51 -07:00
commit 2bf24877a6
7 changed files with 1213 additions and 167 deletions

23
hugolib/benchmark_test.go Normal file
View file

@ -0,0 +1,23 @@
package hugolib
import (
"bytes"
"os"
"testing"
)
func BenchmarkParsePage(b *testing.B) {
f, _ := os.Open("redis.cn.md")
sample := new(bytes.Buffer)
sample.ReadFrom(f)
b.ResetTimer()
for i := 0; i < b.N; i++ {
ReadFrom(sample, "bench")
}
}
func BenchmarkNewPage(b *testing.B) {
for i := 0; i < b.N; i++ {
NewPage("redis.cn.md")
}
}

View file

@ -14,12 +14,15 @@
package hugolib
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/BurntSushi/toml"
"github.com/theplant/blackfriday"
"html/template"
"io"
"io/ioutil"
"launchpad.net/goyaml"
"os"
@ -28,6 +31,7 @@ import (
"sort"
"strings"
"time"
"unicode"
)
var _ = filepath.Base("")
@ -77,14 +81,12 @@ func (p Pages) Sort() { sort.Sort(p) }
func (p Pages) Limit(n int) Pages { return p[0:n] }
func initializePage(filename string) (page Page) {
page = Page{}
page = Page{contentType: "",
File: File{FileName: filename, Extension: "html"},
Node: Node{Keywords: make([]string, 10, 30)},
Params: make(map[string]interface{}),
Markup: "md"}
page.Date, _ = time.Parse("20060102", "20080101")
page.FileName = filename
page.contentType = ""
page.Extension = "html"
page.Params = make(map[string]interface{})
page.Keywords = make([]string, 10, 30)
page.Markup = "md"
page.setSection()
return page
@ -92,6 +94,9 @@ func initializePage(filename string) (page Page) {
func (p *Page) setSection() {
x := strings.Split(p.FileName, string(os.PathSeparator))
if len(x) <= 1 {
return
}
if section := x[len(x)-2]; section != "content" {
p.Section = section
@ -125,6 +130,22 @@ func (page *Page) Layout(l ...string) string {
return strings.ToLower(page.Type()) + "/" + layout + ".html"
}
func ReadFrom(buf io.Reader, name string) (page *Page, err error) {
if len(name) == 0 {
return nil, errors.New("Zero length page name")
}
p := initializePage(name)
if err = p.parse(buf); err != nil {
return
}
p.analyzePage()
return &p, nil
}
// TODO should return errors as well
// TODO new page should return just a page
// TODO initalize separately... load from reader (file, or []byte)
@ -132,7 +153,6 @@ func NewPage(filename string) *Page {
p := initializePage(filename)
if err := p.buildPageFromFile(); err != nil {
fmt.Println(err)
os.Exit(1)
}
p.analyzePage()
@ -145,49 +165,6 @@ func (p *Page) analyzePage() {
p.FuzzyWordCount = int((p.WordCount+100)/100) * 100
}
// TODO //rewrite to use byte methods instead
func (page *Page) parseYamlMetaData(data []byte) ([]string, error) {
var err error
datum, lines := splitPageContent(data, "---", "---")
d, err := page.handleYamlMetaData([]byte(strings.Join(datum, "\n")))
if err != nil {
return lines, err
}
err = page.handleMetaData(d)
return lines, err
}
func (page *Page) parseTomlMetaData(data []byte) ([]string, error) {
var err error
datum, lines := splitPageContent(data, "+++", "+++")
d, err := page.handleTomlMetaData([]byte(strings.Join(datum, "\n")))
if err != nil {
return lines, err
}
err = page.handleMetaData(d)
return lines, err
}
func (page *Page) parseJsonMetaData(data []byte) ([]string, error) {
var err error
datum, lines := splitPageContent(data, "{", "}")
d, err := page.handleJsonMetaData([]byte(strings.Join(datum, "\n")))
if err != nil {
return lines, err
}
err = page.handleMetaData(d)
return lines, err
}
func splitPageContent(data []byte, start string, end string) ([]string, []string) {
lines := strings.Split(string(data), "\n")
datum := lines[0:]
@ -210,18 +187,6 @@ func splitPageContent(data []byte, start string, end string) ([]string, []string
break
}
}
} else { // Start token & end token are the same
for i, line := range lines {
if found == 1 && strings.HasPrefix(line, end) {
datum = lines[1:i]
lines = lines[i+1:]
break
}
if found == 0 && strings.HasPrefix(line, start) {
found = 1
}
}
}
return datum, lines
}
@ -271,7 +236,7 @@ func (page *Page) handleJsonMetaData(datum []byte) (interface{}, error) {
return f, nil
}
func (page *Page) handleMetaData(f interface{}) error {
func (page *Page) update(f interface{}) error {
m := f.(map[string]interface{})
for k, v := range m {
@ -303,7 +268,6 @@ func (page *Page) handleMetaData(f interface{}) error {
page.Status = interfaceToString(v)
default:
// If not one of the explicit values, store in Params
//fmt.Println(strings.ToLower(k))
switch vv := v.(type) {
case string: // handle string values
page.Params[strings.ToLower(k)] = vv
@ -339,25 +303,121 @@ func (page *Page) GetParam(key string) interface{} {
return nil
}
func (page *Page) Err(message string) {
fmt.Println(page.FileName + " : " + message)
var ErrDetectingFrontMatter = errors.New("unable to detect front matter")
var ErrMatchingStartingFrontMatterDelimiter = errors.New("unable to match beginning front matter delimiter")
var ErrMatchingEndingFrontMatterDelimiter = errors.New("unable to match ending front matter delimiter")
func (page *Page) parseFrontMatter(data *bufio.Reader) (err error) {
if err = checkEmpty(data); err != nil {
return
}
var mark rune
if mark, err = chompWhitespace(data); err != nil {
return
}
f := page.detectFrontMatter(mark)
if f == nil {
return ErrDetectingFrontMatter
}
if found, err := beginFrontMatter(data, f); err != nil || !found {
return ErrMatchingStartingFrontMatterDelimiter
}
var frontmatter = new(bytes.Buffer)
for {
line, _, err := data.ReadLine()
if err != nil {
if err == io.EOF {
return ErrMatchingEndingFrontMatterDelimiter
}
return err
}
if bytes.Equal(line, f.markend) {
if f.includeMark {
frontmatter.Write(line)
}
break
}
frontmatter.Write(line)
frontmatter.Write([]byte{'\n'})
}
metadata, err := f.parse(frontmatter.Bytes())
if err != nil {
return
}
if err = page.update(metadata); err != nil {
return
}
return
}
// TODO return error on last line instead of nil
func (page *Page) parseFileHeading(data []byte) ([]string, error) {
if len(data) == 0 {
page.Err("Empty File, skipping")
} else {
switch data[0] {
case '{':
return page.parseJsonMetaData(data)
case '-':
return page.parseYamlMetaData(data)
case '+':
return page.parseTomlMetaData(data)
}
func checkEmpty(data *bufio.Reader) (err error) {
if _, _, err = data.ReadRune(); err != nil {
return errors.New("unable to locate front matter")
}
if err = data.UnreadRune(); err != nil {
return errors.New("unable to unread first charactor in page buffer.")
}
return
}
type frontmatterType struct {
markstart, markend []byte
parse func([]byte) (interface{}, error)
includeMark bool
}
func (page *Page) detectFrontMatter(mark rune) (f *frontmatterType) {
switch mark {
case '-':
return &frontmatterType{[]byte{'-', '-', '-'}, []byte{'-', '-', '-'}, page.handleYamlMetaData, false}
case '+':
return &frontmatterType{[]byte{'+', '+', '+'}, []byte{'+', '+', '+'}, page.handleTomlMetaData, false}
case '{':
return &frontmatterType{[]byte{'{'}, []byte{'}'}, page.handleJsonMetaData, true}
default:
return nil
}
}
func beginFrontMatter(data *bufio.Reader, f *frontmatterType) (bool, error) {
var err error
var peek []byte
if f.includeMark {
peek, err = data.Peek(len(f.markstart))
} else {
peek = make([]byte, len(f.markstart))
_, err = data.Read(peek)
}
if err != nil {
return false, err
}
return bytes.Equal(peek, f.markstart), nil
}
func chompWhitespace(data *bufio.Reader) (r rune, err error) {
for {
r, _, err = data.ReadRune()
if err != nil {
return
}
if unicode.IsSpace(r) {
continue
}
if err := data.UnreadRune(); err != nil {
return r, errors.New("unable to unread first charactor in front matter.")
}
return r, nil
}
return nil, nil
}
func (p *Page) Render(layout ...string) template.HTML {
@ -377,46 +437,50 @@ func (p *Page) ExecuteTemplate(layout string) *bytes.Buffer {
return buffer
}
func (page *Page) readFile() []byte {
var data, err = ioutil.ReadFile(page.FileName)
func (page *Page) readFile() (data []byte, err error) {
data, err = ioutil.ReadFile(page.FileName)
if err != nil {
PrintErr("Error Reading: " + page.FileName)
return nil
return nil, err
}
return data
return data, nil
}
func (page *Page) buildPageFromFile() error {
data := page.readFile()
f, err := os.Open(page.FileName)
if err != nil {
return err
}
return page.parse(bufio.NewReader(f))
}
content, err := page.parseFileHeading(data)
func (page *Page) parse(reader io.Reader) error {
data := bufio.NewReader(reader)
err := page.parseFrontMatter(data)
if err != nil {
return err
}
switch page.Markup {
case "md":
page.convertMarkdown(content)
page.convertMarkdown(data)
case "rst":
page.convertRestructuredText(content)
page.convertRestructuredText(data)
}
return nil
}
func (page *Page) convertMarkdown(lines []string) {
page.RawMarkdown = strings.Join(lines, "\n")
content := string(blackfriday.MarkdownCommon([]byte(page.RawMarkdown)))
func (page *Page) convertMarkdown(lines io.Reader) {
b := new(bytes.Buffer)
b.ReadFrom(lines)
content := string(blackfriday.MarkdownCommon(b.Bytes()))
page.Content = template.HTML(content)
page.Summary = template.HTML(TruncateWordsToWholeSentence(StripHTML(StripShortcodes(content)), summaryLength))
}
func (page *Page) convertRestructuredText(lines []string) {
page.RawMarkdown = strings.Join(lines, "\n")
func (page *Page) convertRestructuredText(lines io.Reader) {
cmd := exec.Command("rst2html.py")
cmd.Stdin = strings.NewReader(page.RawMarkdown)
cmd.Stdin = lines
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {

171
hugolib/page_test.go Normal file
View file

@ -0,0 +1,171 @@
package hugolib
import (
"html/template"
"io"
"strings"
"testing"
)
var EMPTY_PAGE = ""
var SIMPLE_PAGE = `---
title: Simple
---
Simple Page
`
var INVALID_FRONT_MATTER_MISSING = `This is a test`
var INVALID_FRONT_MATTER_SHORT_DELIM = `
--
title: Short delim start
---
Short Delim
`
var INVALID_FRONT_MATTER_SHORT_DELIM_ENDING = `
---
title: Short delim ending
--
Short Delim
`
var INVALID_FRONT_MATTER_LEADING_WS = `
---
title: Leading WS
---
Leading
`
var SIMPLE_PAGE_JSON = `
{
"title": "spf13-vim 3.0 release and new website",
"description": "spf13-vim is a cross platform distribution of vim plugins and resources for Vim.",
"tags": [ ".vimrc", "plugins", "spf13-vim", "vim" ],
"date": "2012-04-06",
"categories": [
"Development",
"VIM"
],
"slug": "spf13-vim-3-0-release-and-new-website"
}
Content of the file goes Here
`
var SIMPLE_PAGE_JSON_MULTIPLE = `
{
"title": "foobar",
"customData": { "foo": "bar" },
"date": "2012-08-06"
}
Some text
`
var SIMPLE_PAGE_JSON_COMPACT = `
{"title":"foobar","customData":{"foo":"bar"},"date":"2012-08-06"}
Text
`
func checkError(t *testing.T, err error, expected string) {
if err == nil {
t.Fatalf("err is nil")
}
if err.Error() != expected {
t.Errorf("err.Error() returned: '%s'. Expected: '%s'", err.Error(), expected)
}
}
func TestDegenerateEmptyPageZeroLengthName(t *testing.T) {
_, err := ReadFrom(strings.NewReader(EMPTY_PAGE), "")
if err == nil {
t.Fatalf("A zero length page name must return an error")
}
checkError(t, err, "Zero length page name")
}
func TestDegenerateEmptyPage(t *testing.T) {
_, err := ReadFrom(strings.NewReader(EMPTY_PAGE), "test")
if err == nil {
t.Fatalf("Expected ReadFrom to return an error when an empty buffer is passed.")
}
checkError(t, err, "unable to locate front matter")
}
func checkPageTitle(t *testing.T, page *Page, title string) {
if page.Title != title {
t.Fatalf("Page title is: %s. Expected %s", page.Title, title)
}
}
func checkPageContent(t *testing.T, page *Page, content string) {
if page.Content != template.HTML(content) {
t.Fatalf("Page content is: %s. Expected %s", page.Content, content)
}
}
func checkPageType(t *testing.T, page *Page, pageType string) {
if page.Type() != pageType {
t.Fatalf("Page type is: %s. Expected: %s", page.Type(), pageType)
}
}
func checkPageLayout(t *testing.T, page *Page, layout string) {
if page.Layout() != layout {
t.Fatalf("Page layout is: %s. Expected: %s", page.Layout(), layout)
}
}
func TestCreateNewPage(t *testing.T) {
p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE), "simple")
if err != nil {
t.Fatalf("Unable to create a page with frontmatter and body content: %s", err)
}
checkPageTitle(t, p, "Simple")
checkPageContent(t, p, "<p>Simple Page</p>\n")
checkPageType(t, p, "page")
checkPageLayout(t, p, "page/single.html")
}
func TestCreatePage(t *testing.T) {
var tests = []struct {
r io.Reader
}{
{strings.NewReader(SIMPLE_PAGE_JSON)},
{strings.NewReader(SIMPLE_PAGE_JSON_MULTIPLE)},
//{strings.NewReader(SIMPLE_PAGE_JSON_COMPACT)},
}
for _, test := range tests {
_, err := ReadFrom(test.r, "page")
if err != nil {
t.Errorf("Unable to parse page: %s", err)
}
}
}
func TestDegenerateInvalidFrontMatterShortDelim(t *testing.T) {
var tests = []struct {
r io.Reader
err string
}{
{strings.NewReader(INVALID_FRONT_MATTER_SHORT_DELIM), "unable to match beginning front matter delimiter"},
{strings.NewReader(INVALID_FRONT_MATTER_SHORT_DELIM_ENDING), "unable to match ending front matter delimiter"},
{strings.NewReader(INVALID_FRONT_MATTER_MISSING), "unable to detect front matter"},
}
for _, test := range tests {
_, err := ReadFrom(test.r, "invalid/front/matter/short/delim")
checkError(t, err, test.err)
}
}
func TestDegenerateInvalidFrontMatterLeadingWhitespace(t *testing.T) {
_, err := ReadFrom(strings.NewReader(INVALID_FRONT_MATTER_LEADING_WS), "invalid/front/matter/leading/ws")
if err != nil {
t.Fatalf("Unable to parse front matter given leading whitespace: %s", err)
}
}

View file

@ -0,0 +1,47 @@
package hugolib
import (
"path/filepath"
"strings"
"testing"
)
var SIMPLE_PAGE_YAML = `---
contenttype: ""
---
Sample Text
`
func TestDegenerateMissingFolderInPageFilename(t *testing.T) {
p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE_YAML), filepath.Join("foobar"))
if err != nil {
t.Fatalf("Error in ReadFrom")
}
if p.Section != "" {
t.Fatalf("No section should be set for a file path: foobar")
}
}
func TestNewPageWithFilePath(t *testing.T) {
toCheck := []map[string]string{
{"input": filepath.Join("sub", "foobar.html"), "expect": "sub"},
{"input": filepath.Join("content", "sub", "foobar.html"), "expect": "sub"},
{"input": filepath.Join("content", "dub", "sub", "foobar.html"), "expect": "sub"},
}
for _, el := range toCheck {
p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE_YAML), el["input"])
if err != nil {
t.Fatalf("Reading from SIMPLE_PAGE_YAML resulted in an error: %s", err)
}
if p.Section != el["expect"] {
t.Fatalf("Section not set to %s for page %s. Got: %s", el["expect"], el["input"], p.Section)
}
}
}
func TestSettingOutFileOnPageContainsCorrectSlashes(t *testing.T) {
s := &Site{Config: Config{}}
p := NewPage(filepath.Join("sub", "foobar"))
s.setOutFile(p)
}

697
hugolib/redis.cn.md Normal file
View file

@ -0,0 +1,697 @@
---
title: The Little Redis Book cn
---
\thispagestyle{empty}
\changepage{}{}{}{-0.5cm}{}{2cm}{}{}{}
![The Little Redis Book cn, By Karl Seguin, Translate By Jason Lai](title.png)\
\clearpage
\changepage{}{}{}{0.5cm}{}{-2cm}{}{}{}
## 关于此书
### 许可证
《The Little Redis Book》是经由Attribution-NonCommercial 3.0 Unported license许可的你不需要为此书付钱。
你可以自由地对此书进行复制分发修改或者展示等操作。当然你必须知道且认可这本书的作者是Karl Seguin译者是赖立维而且不应该将此书用于商业用途。
关于这个**许可证**的*详细描述*在这里:
<http://creativecommons.org/licenses/by-nc/3.0/legalcode>
### 关于作者
作者Karl Seguin是一名在多项技术领域浸淫多年的开发者。他是开源软件计划的活跃贡献者同时也是一名技术作者以及业余演讲者。他写过若干关于Radis的文章以及一些工具。在他的一个面向业余游戏开发者的免费服务里Redis为其中的评级和统计功能提供了支持[mogade.com](http://mogade.com/)。
Karl之前还写了[《The Little MongoDB Book》](http://openmymind.net/2011/3/28/The-Little-MongoDB-Book/)这是一本免费且受好评关于MongoDB的书。
他的博客是<http://openmymind.net>你也可以关注他的Twitter帐号via [@karlseguin](http://twitter.com/karlseguin)。
### 关于译者
译者 赖立维 是一名长在天朝的普通程序员对许多技术都有浓厚的兴趣是开源软件的支持者Emacs的轻度使用者。
虽然译者已经很认真地对待这次翻译,但是限于水平有限,肯定会有不少错漏,如果发现该书的翻译有什么需要修改,可以通过他的邮箱与他联系。他的邮箱是<jasonlai256@gmail.com>
### 致谢
必须特别感谢[Perry Neal](https://twitter.com/perryneal)一直以来的指导,我的眼界、触觉以及激情都来源于你。你为我提供了无价的帮助,感谢你。
### 最新版本
此书的最新有效资源在:
<http://github.com/karlseguin/the-little-redis-book>
中文版是英文版的一个分支,最新的中文版本在:
<https://github.com/JasonLai256/the-little-redis-book>
\clearpage
## 简介
最近几年来,关于持久化和数据查询的相关技术,其需求已经增长到了让人惊讶的程度。可以断言,关系型数据库再也不是放之四海皆准。换一句话说,围绕数据的解决方案不可能再只有唯一一种。
对于我来说在众多新出现的解决方案和工具里最让人兴奋的无疑是Redis。为什么首先是因为其让人不可思议的容易学习只需要简短的几个小时学习时间就能对Redis有个大概的认识。还有Redis在处理一组特定的问题集的同时能保持相当的通用性。更准确地说就是Redis不会尝试去解决关于数据的所有事情。在你足够了解Redis后事情就会变得越来越清晰什么是可行的什么是不应该由Redis来处理的。作为一名开发人员如此的经验当是相当的美妙。
当你能仅使用Redis去构建一个完整系统时我想大多数人将会发现Redis能使得他们的许多数据方案变得更为通用不论是一个传统的关系型数据库一个面向文档的系统或是其它更多的东西。这是一种用来实现某些特定特性的解决方法。就类似于一个索引引擎你不会在Lucene上构建整个程序但当你需要足够好的搜索为什么不使用它呢这对你和你的用户都有好处。当然关于Redis和索引引擎之间相似性的讨论到此为止。
本书的目的是向读者传授掌握Redis所需要的基本知识。我们将会注重于学习Redis的5种数据结构并研究各种数据建模方法。我们还会接触到一些主要的管理细节和调试技巧。
## 入门
每个人的学习方式都不一样有的人喜欢亲自实践学习有的喜欢观看教学视频还有的喜欢通过阅读来学习。对于Redis没有什么比亲自实践学习来得效果更好的了。Redis的安装非常简单。而且通过随之安装的一个简单的命令解析程序就能处理我们想做的一切事情。让我们先花几分钟的时间把Redis安装到我们的机器上。
### Windows平台
Redis并没有官方支持Windows平台但还是可供选择。你不会想在这里配置实际的生产环境不过在我过往的开发经历里并没有感到有什么限制。
首先进入<https://github.com/dmajkic/redis/downloads>,然后下载最新的版本(应该会在列表的最上方)。
获取zip文件然后根据你的系统架构打开`64bit`或`32bit`文件夹。
### *nix和MacOSX平台
对于*nix和MacOSX平台的用户从源文件来安装是你的最佳选择。通过最新的版本号来选择有效地址于<http://redis.io/download>。在编写此书的时候最新的版本是2.4.6,我们可以运行下面的命令来安装该版本:
wget http://redis.googlecode.com/files/redis-2.4.6.tar.gz
tar xzf redis-2.4.6.tar.gz
cd redis-2.4.6
make
当然Redis同样可以通过套件管理程序来安装。例如使用Homebrew的MaxOSX用户可以只键入`brew install redis`即可。)
如果你是通过源文件来安装,二进制可执行文件会被放置在`src`目录里。通过运行`cd src`可跳转到`src`目录。
### 运行和连接Redis
如果一切都工作正常那Redis的二进制文件应该已经可以曼妙地跳跃于你的指尖之下。Redis只有少量的可执行文件我们将着重于Redis的服务器和命令行界面一个类DOS的客户端。首先让我们来运行服务器。在Windows平台双击`redis-server`,在*nix/MacOSX平台则运行`./redis-server`.
如果你仔细看了启动信息,你会看到一个警告,指没能找到`redis.conf`文件。Redis将会采用内置的默认设置这对于我们将要做的已经足够了。
然后,通过双击`redis-cli`Windows平台或者运行`./redis-cli`*nix/MacOSX平台启动Redis的控制台。控制台将会通过默认的端口6379来连接本地运行的服务器。
可以在命令行界面键入`info`命令来查看一切是不是都运行正常。你会很乐意看到这么一大组关键字-值key-value对的显示这为我们查看服务器的状态提供了大量有效信息。
如果在上面的启动步骤里遇到什么问题,我建议你到[Redis的官方支持组](https://groups.google.com/forum/#!forum/redis-db)里获取帮助。
## 驱动Redis
很快你就会发现Redis的API就如一组定义明确的函数那般容易理解。Redis具有让人难以置信的简单性其操作过程也同样如此。这意味着无论你是使用命令行程序或是使用你喜欢的语言来驱动整体的感觉都不会相差多少。因此相对于命令行程序如果你更愿意通过一种编程语言去驱动Redis你不会感觉到有任何适应的问题。如果真想如此可以到Redis的[客户端推荐页面](http://redis.io/clients)下载适合的Redis载体。
\clearpage
## 第1章 - 基础知识
是什么使Redis显得这么特别Redis具体能解决什么类型的问题要实际应用Redis开发者必须储备什么知识在我们能回答这么一些问题之前我们需要明白Redis到底是什么。
Redis通常被人们认为是一种持久化的存储器关键字-值型存储in-memory persistent key-value store。我认为这种对Redis的描述并不太准确。Redis的确是将所有的数据存放于存储器更多是是按位存储而且也确实通过将数据写入磁盘来实现持久化但是Redis的实际意义比单纯的关键字-值型存储要来得深远。纠正脑海里的这种误解观点非常关键否则你对于Redis之道以及其应用的洞察力就会变得越发狭义。
事实是Redis引入了5种不同的数据结构只有一个是典型的关键字-值型结构。理解Redis的关键就在于搞清楚这5种数据结构其工作的原理都是如何有什么关联方法以及你能怎样应用这些数据结构去构建模型。首先让我们来弄明白这些数据结构的实际意义。
应用上面提及的数据结构概念到我们熟悉的关系型数据库里我们可以认为其引入了一个单独的数据结构——表格。表格既复杂又灵活基于表格的存储和管理没有多少东西是你不能进行建模的。然而这种通用性并不是没有缺点。具体来说就是事情并不是总能达到假设中的简单或者快速。相对于这种普遍适用one-size-fits-all的结构体系我们可以使用更为专门化的结构体系。当然因此可能有些事情我们会完成不了(至少,达不到很好的程度)。但话说回来,这样做就能确定我们可以获得想象中的简单性和速度吗?
针对特定类型的问题使用特定的数据结构我们不就是这样进行编程的吗你不会使用一个散列表去存储每份数据也不会使用一个标量变量去存储。对我来说这正是Redis的做法。如果你需要处理标量、列表、散列或者集合为什么不直接就用标量、列表、散列和集合去存储他们为什么不是直接调用`exists(key)`去检测一个已存在的值而是要调用其他比O(1)(常量时间查找,不会因为待处理元素的增长而变慢)慢的操作?
### 数据库Databases
与你熟悉的关系型数据库一致Redis有着相同的数据库基本概念即一个数据库包含一组数据。典型的数据库应用案例是将一个程序的所有数据组织起来使之与另一个程序的数据保持独立。
在Redis里数据库简单的使用一个数字编号来进行辨认默认数据库的数字编号是`0`。如果你想切换到一个不同的数据库,你可以使用`select`命令来实现。在命令行界面里键入`select 1`Redis应该会回复一条`OK`的信息,然后命令行界面里的提示符会变成类似`redis 127.0.0.1:6379[1]>`这样。如果你想切换回默认数据库,只要在命令行界面键入`select 0`即可。
### 命令、关键字和值Commands, Keys and Values
Redis不仅仅是一种简单的关键字-值型存储从其核心概念来看Redis的5种数据结构中的每一个都至少有一个关键字和一个值。在转入其它关于Redis的有用信息之前我们必须理解关键字和值的概念。
关键字Keys是用来标识数据块。我们将会很常跟关键字打交道不过在现在明白关键字就是类似于`users:leto`这样的表述就足够了。一般都能很好地理解到,这样关键字包含的信息是一个名为`leto`的用户。这个关键字里的冒号没有任何特殊含义对于Redis而言使用分隔符来组织关键字是很常见的方法。
Values是关联于关键字的实际值可以是任何东西。有时候你会存储字符串有时候是整数还有时候你会存储序列化对象使用JSON、XML或其他格式。在大多数情况下Redis会把值看做是一个字节序列而不会关注它们实质上是什么。要注意不同的Redis载体处理序列化会有所不同一些会让你自己决定。因此在这本书里我们将仅讨论字符串、整数和JSON。
现在让我们活动一下手指吧。在命令行界面键入下面的命令:
set users:leto "{name: leto, planet: dune, likes: [spice]}"
这就是Redis命令的基本构成。首先我们要有一个确定的命令在上面的语句里就是`set`。然后就是相应的参数,`set`命令接受两个参数,包括要设置的关键字,以及相应要设置的值。很多的情况是,命令接受一个关键字(当这种情况出现,其经常是第一个参数)。你能想到如何去获取这个值吗?我想你会说(当然一时拿不准也没什么):
get users:leto
关键字和值的是Redis的基本概念而`get`和`set`命令是对此最简单的使用。你可以创建更多的用户,去尝试不同类型的关键字以及不同的值,看看一些不同的组合。
### 查询Querying
随着学习的持续深入两件事情将变得清晰起来。对于Redis而言关键字就是一切而值是没有任何意义。更通俗来看就是Redis不允许你通过值来进行查询。回到上面的例子我们就不能查询生活在`dune`行星上的用户。
对许多人来说这会引起一些担忧。在我们生活的世界里数据查询是如此的灵活和强大而Redis的方式看起来是这么的原始和不高效。不要让这些扰乱你太久。要记住Redis不是一种普遍使用one-size-fits-all的解决方案确实存在这么一些事情是不应该由Redis来解决的因为其查询的限制。事实上在考虑了这些情况后你会找到新的方法去构建你的数据。
很快我们就能看到更多实际的用例。很重要的一点是我们要明白关于Redis的这些基本事实。这能帮助我们弄清楚为什么值可以是任何东西因为Redis从来不需要去读取或理解它们。而且这也可以帮助我们理清思路然后去思考如何在这个新世界里建立模型。
### 存储器和持久化Memory and Persistence
我们之前提及过Redis是一种持久化的存储器内存储in-memory persistent store。对于持久化默认情况下Redis会根据已变更的关键字数量来进行判断然后在磁盘里创建数据库的快照snapshot。你可以对此进行设置如果X个关键字已变更那么每隔Y秒存储数据库一次。默认情况下如果1000个或更多的关键字已变更Redis会每隔60秒存储数据库而如果9个或更少的关键字已变更Redis会每隔15分钟存储数据库。
除了创建磁盘快照外Redis可以在附加模式下运行。任何时候如果有一个关键字变更一个单一附加append-only的文件会在磁盘里进行更新。在一些情况里虽然硬件或软件可能发生错误但用那60秒有效数据存储去换取更好性能是可以接受的。而在另一些情况里这种损失就难以让人接受Redis为你提供了选择。在第5章里我们将会看到第三种选择其将持久化任务减荷到一个从属数据库里。
至于存储器Redis会将所有数据都保留在存储器中。显而易见运行Redis具有不低的成本因为RAM仍然是最昂贵的服务器硬件部件。
我很清楚有一些开发者对即使是一点点的数据空间都是那么的敏感。一本《威廉·莎士比亚全集》需要近5.5MB的存储空间。对于缩放的需求其它的解决方案趋向于IO-bound或者CPU-bound。这些限制RAM或者IO将会需要你去理解更多机器实际依赖的数据类型以及应该如何去进行存储和查询。除非你是存储大容量的多媒体文件到Redis中否则存储器内存储应该不会是一个问题。如果这对于一个程序是个问题你就很可能不会用IO-bound的解决方案。
Redis有虚拟存储器的支持。然而这个功能已经被认为是失败的了通过Redis的开发者而且它的使用已经被废弃了。
从另一个角度来看一本5.5MB的《威廉·莎士比亚全集》可以通过压缩减小到近2MB。当然Redis不会自动对值进行压缩但是因为其将所有值都看作是字节没有什么限制让你不能对数据进行压缩/解压,通过牺牲处理时间来换取存储空间。)
### 整体来看Putting It Together
我们已经接触了好几个高层次的主题。在继续深入Redis之前我想做的最后一件事情是将这些主题整合起来。这些主题包括查询的限制数据结构以及Redis在存储器内存储数据的方法。
当你将这3个主题整合起来你最终会得出一个绝妙的结论速度。一些人可能会想当然Redis会很快速要知道所以的东西都在存储器里。但这仅仅是其中的一部分让Redis闪耀的真正原因是其不同于其它解决方案的特殊数据结构。
能有多快速这依赖于很多东西包括你正在使用着哪个命令数据的类型等等。但Redis的性能测试是趋向于数万或数十万次操作**每秒**。你可以通过运行`redis-benchmark`(就在`redis-server`和`redis-cli`的同一个文件夹里)来进行测试。
我曾经试过将一组使用传统模型的代码转向使用Redis。在传统模型里运行一个我写的载入测试需要超过5分钟的时间来完成。而在Redis里只需要150毫秒就完成了。你不会总能得到这么好的收获但希望这能让你对我们所谈的东西有更清晰的理解。
理解Redis的这个特性很重要因为这将影响到你如何去与Redis进行交互。拥有SQL背景的程序员通常会致力于让数据库的数据往返次数减至最小。这对于任何系统都是个好建议包括Redis。然而考虑到我们是在处理比较简单的数据结构有时候我们还是需要与Redis服务器频繁交互以达到我们的目的。刚开始的时候可能会对这种数据访问模式感到不太自然。实际上相对于我们通过Redis获得的高性能而言这仅仅是微不足道的损失。
### 小结
虽然我们只接触和摆弄了Redis的冰山一角但我们讨论的主题已然覆盖了很大范围内的东西。如果觉得有些事情还是不太清楚例如查询不用为此而担心在下一章我们将会继续深入探讨希望你的问题都能得到解答。
这一章的要点包括:
* 关键字Keys是用于标识一段数据的一个字符串
* 值Values是一段任意的字节序列Redis不会关注它们实质上是什么
* Redis展示了也实现了5种专门的数据结构
* 上面的几点使得Redis快速而且容易使用但要知道Redis并不适用于所有的应用场景
\clearpage
## 第2章 - 数据结构
现在开始将探究Redis的5种数据结构我们会解释每种数据结构都是什么包含了什么有效的方法Method以及你能用这些数据结构处理哪些类型的特性和数据。
目前为止我们所知道的Redis构成仅包括命令、关键字和值还没有接触到关于数据结构的具体概念。当我们使用`set`命令时Redis是怎么知道我们是在使用哪个数据结构其解决方法是每个命令都相对应于一种特定的数据结构。例如当你使用`set`命令,你就是将值存储到一个字符串数据结构里。而当你使用`hset`命令你就是将值存储到一个散列数据结构里。考虑到Redis的关键字集很小这样的机制具有相当的可管理性。
**[Redis的网站](http://redis.io/commands)里有着非常优秀的参考文档,没有任何理由去重造轮子。但为了搞清楚这些数据结构的作用,我们将会覆盖那些必须知道的重要命令。**
没有什么事情比高兴的玩和试验有趣的东西来得更重要的了。在任何时候,你都能通过键入`flushdb`命令将你数据库里的所有值清除掉,因此,不要再那么害羞了,去尝试做些疯狂的事情吧!
### 字符串Strings)
在Redis里字符串是最基本的数据结构。当你在思索着关键字-值对时你就是在思索着字符串数据结构。不要被名字给搞混了如之前说过的你的值可以是任何东西。我更喜欢将他们称作“标量”Scalars但也许只有我才这样想。
我们已经看到了一个常见的字符串使用案例,即通过关键字存储对象的实例。有时候,你会频繁地用到这类操作:
set users:leto "{name: leto, planet: dune, likes: [spice]}"
除了这些外Redis还有一些常用的操作。例如`strlen <key>`能用来获取一个关键字对应值的长度;`getrange <key> <start> <end>`将返回指定范围内的关键字对应值;`append <key> <value>`会将value附加到已存在的关键字对应值中如果该关键字并不存在则会创建一个新的关键字-值对)。不要犹豫,去试试看这些命令吧。下面是我得到的:
> strlen users:leto
(integer) 42
> getrange users:leto 27 40
"likes: [spice]"
> append users:leto " OVER 9000!!"
(integer) 54
现在你可能会想这很好但似乎没有什么意义。你不能有效地提取出一段范围内的JSON文件或者为其附加一些值。你是对的这里的经验是一些命令尤其是关于字符串数据结构的只有在给定了明确的数据类型后才会有实际意义。
之前我们知道了Redis不会去关注你的值是什么东西。通常情况下这没有错。然而一些字符串命令是专门为一些类型或值的结构而设计的。作为一个有些含糊的用例我们可以看到对于一些自定义的空间效率很高的space-efficient串行化对象`append`和`getrange`命令将会很有用。对于一个更为具体的用例,我们可以再看一下`incr`、`incrby`、`decr`和`decrby`命令。这些命令会增长或者缩减一个字符串数据结构的值:
> incr stats:page:about
(integer) 1
> incr stats:page:about
(integer) 2
> incrby ratings:video:12333 5
(integer) 5
> incrby ratings:video:12333 3
(integer) 8
由此你可以想象到Redis的字符串数据结构能很好地用于分析用途。你还可以去尝试增长`users:leto`(一个不是整数的值),然后看看会发生什么(应该会得到一个错误)。
更为进阶的用例是`setbit`和`getbit`命令。“今天我们有多少个独立用户访问”是个在Web应用里常见的问题有一篇[精彩的博文](http://blog.getspool.com/2011/11/29/fast-easy-realtime-metrics-using-redis-bitmaps/)在里面可以看到Spool是如何使用这两个命令有效地解决此问题。对于1.28亿个用户一部笔记本电脑在不到50毫秒的时间里就给出了答复而且只用了16MB的存储空间。
最重要的事情不是在于你是否明白位图Bitmaps)的工作原理或者Spool是如何去使用这些命令而是应该要清楚Redis的字符串数据结构比你当初所想的要有用许多。然而最常见的应用案例还是上面我们给出的存储对象简单或复杂和计数。同时由于通过关键字来获取一个值是如此之快字符串数据结构很常被用来缓存数据。
### 散列Hashes
我们已经知道把Redis称为一种关键字-值型存储是不太准确的散列数据结构是一个很好的例证。你会看到在很多方面里散列数据结构很像字符串数据结构。两者显著的区别在于散列数据结构提供了一个额外的间接层一个域Field。因此散列数据结构中的`set`和`get`是:
hset users:goku powerlevel 9000
hget users:goku powerlevel
相关的操作还包括在同一时间设置多个域、同一时间获取多个域、获取所有的域和值、列出所有的域或者删除指定的一个域:
hmset users:goku race saiyan age 737
hmget users:goku race powerlevel
hgetall users:goku
hkeys users:goku
hdel users:goku age
如你所见,散列数据结构比普通的字符串数据结构具有更多的可操作性。我们可以使用一个散列数据结构去获得更精确的描述,是存储一个用户,而不是一个序列化对象。从而得到的好处是能够提取、更新和删除具体的数据片段,而不必去获取或写入整个值。
对于散列数据结构,可以从一个经过明确定义的对象的角度来考虑,例如一个用户,关键之处在于要理解他们是如何工作的。从性能上的原因来看,这是正确的,更具粒度化的控制可能会相当有用。在下一章我们将会看到,如何用散列数据结构去组织你的数据,使查询变得更为实效。在我看来,这是散列真正耀眼的地方。
### 列表Lists
对于一个给定的关键字,列表数据结构让你可以存储和处理一组值。你可以添加一个值到列表里、获取列表的第一个值或最后一个值以及用给定的索引来处理值。列表数据结构维护了值的顺序,提供了基于索引的高效操作。为了跟踪在网站里注册的最新用户,我们可以维护一个`newusers`的列表:
lpush newusers goku
ltrim newusers 0 50
**(译注:`ltrim`命令的具体构成是`LTRIM Key start stop`。要理解`ltrim`命令首先要明白Key所存储的值是一个列表理论上列表可以存放任意个值。对于指定的列表根据所提供的两个范围参数start和stop`ltrim`命令会将指定范围外的值都删除掉,只留下范围内的值。)**
首先我们将一个新用户推入到列表的前端然后对列表进行调整使得该列表只包含50个最近被推入的用户。这是一种常见的模式。`ltrim`是一个具有O(N)时间复杂度的操作N是被删除的值的数量。从上面的例子来看我们总是在插入了一个用户后再进行列表调整实际上其将具有O(1)的时间复杂度因为N将永远等于1的常数性能。
这是我们第一次看到一个关键字的对应值索引另一个值。如果我们想要获取最近的10个用户的详细资料我们可以运行下面的组合操作
keys = redis.lrange('newusers', 0, 10)
redis.mget(*keys.map {|u| "users:#{u}"})
我们之前谈论过关于多次往返数据的模式上面的两行Ruby代码为我们进行了很好的演示。
当然,对于存储和索引关键字的功能,并不是只有列表数据结构这种方式。值可以是任意的东西,你可以使用列表数据结构去存储日志,也可以用来跟踪用户浏览网站时的路径。如果你过往曾构建过游戏,你可能会使用列表数据结构去跟踪用户的排队活动。
### 集合
集合数据结构常常被用来存储只能唯一存在的值,并提供了许多的基于集合的操作,例如并集。集合数据结构没有对值进行排序,但是其提供了高效的基于值的操作。使用集合数据结构的典型用例是朋友名单的实现:
sadd friends:leto ghanima paul chani jessica
sadd friends:duncan paul jessica alia
不管一个用户有多少个朋友我们都能高效地O(1)时间复杂度识别出用户X是不是用户Y的朋友
sismember friends:leto jessica
sismember friends:leto vladimir
而且,我们可以查看两个或更多的人是不是有共同的朋友:
sinter friends:leto friends:duncan
甚至可以在一个新的关键字里存储结果:
sinterstore friends:leto_duncan friends:leto friends:duncan
有时候需要对值的属性进行标记和跟踪处理,但不能通过简单的复制操作完成,集合数据结构是解决此类问题的最好方法之一。当然,对于那些需要运用集合操作的地方(例如交集和并集),集合数据结构就是最好的选择。
### 分类集合Sorted Sets
最后也是最强大的数据结构是分类集合数据结构。如果说散列数据结构类似于字符串数据结构主要区分是域field的概念那么分类集合数据结构就类似于集合数据结构主要区分是标记score的概念。标记提供了排序sorting和秩划分ranking的功能。如果我们想要一个秩分类的朋友名单可以这样做
zadd friends:duncan 70 ghanima 95 paul 95 chani 75 jessica 1 vladimir
对于`duncan`的朋友要怎样计算出标记score为90或更高的人数
zcount friends:duncan 90 100
如何获取`chani`在名单里的秩rank
zrevrank friends:duncan chani
**(译注:`zrank`命令的具体构成是`ZRANK Key menber`要知道Key存储的Sorted Set默认是根据Score对各个menber进行升序的排列该命令就是用来获取menber在该排列里的次序这就是所谓的秩。**
我们使用了`zrevrank`命令而不是`zrank`命令这是因为Redis的默认排序是从低到高但是在这个例子里我们的秩划分是从高到低。对于分类集合数据结构最常见的应用案例是用来实现排行榜系统。事实上对于一些基于整数排序且能以标记score来进行有效操作的东西使用分类集合数据结构来处理应该都是不错的选择。
### 小结
对于Redis的5种数据结构我们进行了高层次的概述。一件有趣的事情是相对于最初构建时的想法你经常能用Redis创造出一些更具实效的事情。对于字符串数据结构和分类集合数据结构的使用很有可能存在一些构建方法是还没有人想到的。当你理解了那些常用的应用案例后你将发现Redis对于许多类型的问题都是很理想的选择。还有不要因为Redis展示了5种数据结构和相应的各种方法就认为你必须要把所有的东西都用上。只使用一些命令去构建一个特性是很常见的。
\clearpage
## 第3章 - 使用数据结构
在上一章里我们谈论了Redis的5种数据结构对于一些可能的用途也给出了用例。现在是时候来看看一些更高级但依然很常见的主题和设计模式。
### 大O表示法Big O Notation
在本书中我们之前就已经看到过大O表示法包括O(1)和O(N)的表示。大O表示法的惯常用途是描述一些用于处理一定数量元素的行为的综合表现。在Redis里对于一个要处理一定数量元素的命令大O表示法让我们能了解该命令的大概运行速度。
在Redis的文档里每一个命令的时间复杂度都用大O表示法进行了描述还能知道各命令的具体性能会受什么因素影响。让我们来看看一些用例。
常数时间复杂度O(1)被认为是最快速的无论我们是在处理5个元素还是5百万个元素最终都能得到相同的性能。对于`sismember`命令其作用是告诉我们一个值是否属于一个集合时间复杂度为O(1)。`sismember`命令很强大很大部分的原因是其高效的性能特征。许多Redis命令都具有O(1)的时间复杂度。
对数时间复杂度O(log(N))被认为是第二快速的,其通过使需扫描的区间不断皱缩来快速完成处理。使用这种“分而治之”的方式,大量的元素能在几个迭代过程里被快速分解完整。`zadd`命令的时间复杂度就是O(log(N))其中N是在分类集合中的元素数量。
再下来就是线性时间复杂度O(N)在一个表格的非索引列里进行查找就需要O(N)次操作。`ltrim`命令具有O(N)的时间复杂度,但是,在`ltrim`命令里N不是列表所拥有的元素数量而是被删除的元素数量。从一个具有百万元素的列表里用`ltrim`命令删除1个元素要比从一个具有一千个元素的列表里用`ltrim`命令删除10个元素来的快速实际上两者很可能会是一样快因为两个时间都非常的小
根据给定的最小和最大的值的标记,`zremrangebyscore`命令会在一个分类集合里进行删除元素操作其时间复杂度是O(log(N)+M)。这看起来似乎有点儿杂乱通过阅读文档可以知道这里的N指的是在分类集合里的总元素数量而M则是被删除的元素数量。可以看出对于性能而言被删除的元素数量很可能会比分类集合里的总元素数量更为重要。
**(译注:`zremrangebyscore`命令的具体构成是`ZREMRANGEBYSCORE Key max mix`。)**
对于`sort`命令其时间复杂度为O(N+M*log(M)),我们将会在下一章谈论更多的相关细节。从`sort`命令的性能特征来看可以说这是Redis里最复杂的一个命令。
还存在其他的时间复杂度描述包括O(N^2)和O(C^N)。随着N的增大其性能将急速下降。在Redis里没有任何一个命令具有这些类型的时间复杂度。
值得指出的一点是在Redis里当我们发现一些操作具有O(N)的时间复杂度时,我们可能可以找到更为好的方法去处理。
**译注对于Big O Notation相信大家都非常的熟悉虽然原文仅仅是对该表示法进行简单的介绍但限于个人的算法知识和文笔水平实在有限此小节的翻译让我头痛颇久最终成果也确实难以让人满意望见谅。**
### 仿多关键字查询Pseudo Multi Key Queries
时常你会想通过不同的关键字去查询相同的值。例如你会想通过电子邮件当用户开始登录时去获取用户的具体信息或者通过用户id在用户登录后去获取。有一种很不实效的解决方法其将用户对象分别放置到两个字符串值里去
set users:leto@dune.gov "{id: 9001, email: 'leto@dune.gov', ...}"
set users:9001 "{id: 9001, email: 'leto@dune.gov', ...}"
这种方法很糟糕,如此不但会产生两倍数量的内存,而且这将会成为数据管理的恶梦。
如果Redis允许你将一个关键字链接到另一个的话可能情况会好很多可惜Redis并没有提供这样的功能而且很可能永远都不会提供。Redis发展到现在其开发的首要目的是要保持代码和API的整洁简单关键字链接功能的内部实现并不符合这个前提对于关键字我们还有很多相关方法没有谈论到。其实Redis已经提供了解决的方法散列。
使用散列数据结构,我们可以摆脱重复的缠绕:
set users:9001 "{id: 9001, email: leto@dune.gov, ...}"
hset users:lookup:email leto@dune.gov 9001
我们所做的是使用域来作为一个二级索引然后去引用单个用户对象。要通过id来获取用户信息我们可以使用一个普通的`get`命令:
get users:9001
而如果想通过电子邮箱来获取用户信息,我们可以使用`hget`命令再配合使用`get`命令Ruby代码
id = redis.hget('users:lookup:email', 'leto@dune.gov')
user = redis.get("users:#{id}")
你很可能将会经常使用这类用法。在我看来,这就是散列真正耀眼的地方。在你了解这类用法之前,这可能不是一个明显的用例。
### 引用和索引References and Indexes
我们已经看过几个关于值引用的用例包括介绍列表数据结构时的用例以及在上面使用散列数据结构来使查询更灵活一些。进行归纳后会发现对于那些值与值间的索引和引用我们都必须手动的去管理。诚实来讲这确实会让人有点沮丧尤其是当你想到那些引用相关的操作如管理、更新和删除等都必须手动的进行时。在Redis里这个问题还没有很好的解决方法。
我们已经看到,集合数据结构很常被用来实现这类索引:
sadd friends:leto ghanima paul chani jessica
这个集合里的每一个成员都是一个Redis字符串数据结构的引用而每一个引用的值则包含着用户对象的具体信息。那么如果`chani`改变了她的名字,或者删除了她的帐号,应该如何处理?从整个朋友圈的关系结构来看可能会更好理解,我们知道,`chani`也有她的朋友:
sadd friends_of:chani leto paul
如果你有什么待处理情况像上面那样那在维护成本之外还会有对于额外索引值的处理和存储空间的成本。这可能会令你感到有点退缩。在下一小节里我们将会谈论减少使用额外数据交互的性能成本的一些方法在第1章我们粗略地讨论了下
如果你确实在担忧着这些情况,其实,关系型数据库也有同样的开销。索引需要一定的存储空间,必须通过扫描或查找,然后才能找到相应的记录。其开销也是存在的,当然他们对此做了很多的优化工作,使之变得更为有效。
再次说明需要在Redis里手动地管理引用确实是颇为棘手。但是对于你关心的那些问题包括性能或存储空间等应该在经过测试后才会有真正的理解。我想你会发现这不会是一个大问题。
### 数据交互和流水线Round Trips and Pipelining
我们已经提到过与服务器频繁交互是Redis的一种常见模式。这类情况可能很常出现为了使我们能获益更多值得仔细去看看我们能利用哪些特性。
许多命令能接受一个或更多的参数也有一种关联命令sister-command可以接受多个参数。例如早前我们看到过`mget`命令,接受多个关键字,然后返回值:
keys = redis.lrange('newusers', 0, 10)
redis.mget(*keys.map {|u| "users:#{u}"})
或者是`sadd`命令,能添加一个或多个成员到集合里:
sadd friends:vladimir piter
sadd friends:paul jessica leto "leto II" chani
Redis还支持流水线功能。通常情况下当一个客户端发送请求到Redis后在发送下一个请求之前必须等待Redis的答复。使用流水线功能你可以发送多个请求而不需要等待Redis响应。这不但减少了网络开销还能获得性能上的显著提高。
值得一提的是Redis会使用存储器去排列命令因此批量执行命令是一个好主意。至于具体要多大的批量将取决于你要使用什么命令更明确来说该参数有多大。另一方面来看如果你要执行的命令需要差不多50个字符的关键字你大概可以对此进行数千或数万的批量操作。
对于不同的Redis载体在流水线里运行命令的方式会有所差异。在Ruby里你传递一个代码块到`pipelined`方法:
redis.pipelined do
9001.times do
redis.incr('powerlevel')
end
end
正如你可能猜想到的,流水线功能可以实际地加速一连串命令的处理。
### 事务Transactions
每一个Redis命令都具有原子性包括那些一次处理多项事情的命令。此外对于使用多个命令Redis支持事务功能。
你可能不知道但Redis实际上是单线程运行的这就是为什么每一个Redis命令都能够保证具有原子性。当一个命令在执行时没有其他命令会运行我们会在往后的章节里简略谈论一下Scaling。在你考虑到一些命令去做多项事情时这会特别的有用。例如
`incr`命令实际上就是一个`get`命令然后紧随一个`set`命令。
`getset`命令设置一个新的值然后返回原始值。
`setnx`命令首先测试关键字是否存在,只有当关键字不存在时才设置值
虽然这些都很有用,但在实际开发时,往往会需要运行具有原子性的一组命令。若要这样做,首先要执行`multi`命令,紧随其后的是所有你想要执行的命令(作为事务的一部分),最后执行`exec`命令去实际执行命令,或者使用`discard`命令放弃执行命令。Redis的事务功能保证了什么
* 事务中的命令将会按顺序地被执行
* 事务中的命令将会如单个原子操作般被执行(没有其它的客户端命令会在中途被执行)
* 事务中的命令要么全部被执行,要么不会执行
你可以(也应该)在命令行界面对事务功能进行一下测试。还有一点要注意到,没有什么理由不能结合流水线功能和事务功能。
multi
hincrby groups:1percent balance -9000000000
hincrby groups:99percent balance 9000000000
exec
最后Redis能让你指定一个关键字或多个关键字当关键字有改变时可以查看或者有条件地应用一个事务。这是用于当你需要获取值且待运行的命令基于那些值时所有都在一个事务里。对于上面展示的代码我们不能去实现自己的`incr`命令,因为一旦`exec`命令被调用,他们会全部被执行在一块。我们不能这么做:
redis.multi()
current = redis.get('powerlevel')
redis.set('powerlevel', current + 1)
redis.exec()
**译注虽然Redis是单线程运行的但是我们可以同时运行多个Redis客户端进程常见的并发问题还是会出现。像上面的代码在`get`运行之后,`set`运行之前,`powerlevel`的值可能会被另一个Redis客户端给改变从而造成错误。**
这些不是Redis的事务功能的工作。但是如果我们增加一个`watch`到`powerlevel`,我们可以这样做:
redis.watch('powerlevel')
current = redis.get('powerlevel')
redis.multi()
redis.set('powerlevel', current + 1)
redis.exec()
在我们调用`watch`后,如果另一个客户端改变了`powerlevel`的值,我们的事务将会运行失败。如果没有客户端改变`powerlevel`的值,那么事务会继续工作。我们可以在一个循环里运行这些代码,直到其能正常工作。
### 关键字反模式Keys Anti-Pattern
在下一章中,我们将会谈论那些没有确切关联到数据结构的命令,其中的一些是管理或调试工具。然而有一个命令我想特别地在这里进行谈论:`keys`命令。这个命令需要一个模式,然后查找所有匹配的关键字。这个命令看起来很适合一些任务,但这不应该用在实际的产品代码里。为什么?因为这个命令通过线性扫描所有的关键字来进行匹配。或者,简单地说,这个命令太慢了。
人们会如此去使用这个命令一般会用来构建一个本地的Bug追踪服务。每一个帐号都有一个`id`,你可能会通过一个看起来像`bug:account_id:bug_id`的关键字把每一个Bug存储到一个字符串数据结构值中去。如果你在任何时候需要查询一个帐号的Bug显示它们或者当用户删除了帐号时删除掉这些Bugs你可能会尝试去使用`keys`命令:
keys bug:1233:*
更好的解决方法应该使用一个散列数据结构,就像我们可以使用散列数据结构来提供一种方法去展示二级索引,因此我们可以使用域来组织数据:
hset bugs:1233 1 "{id:1, account: 1233, subject: '...'}"
hset bugs:1233 2 "{id:2, account: 1233, subject: '...'}"
从一个帐号里获取所有的Bug标识可以简单地调用`hkeys bugs:1233`。去删除一个指定的Bug可以调用`hdel bugs:1233 2`。如果要删除了一个帐号,可以通过`del bugs:1233`把关键字删除掉。
### 小结
结合这一章以及前一章希望能让你得到一些洞察力了解如何使用Redis去支持Power实际项目。还有其他的模式可以让你去构建各种类型的东西但真正的关键是要理解基本的数据结构。你将能领悟到这些数据结构是如何能够实现你最初视角之外的东西。
\clearpage
## 第4章 超越数据结构
5种数据结构组成了Redis的基础其他没有关联特定数据结构的命令也有很多。我们已经看过一些这样的命令`info`, `select`, `flushdb`, `multi`, `exec`, `discard`, `watch`和`keys `。这一章将看看其他的一些重要命令。
### 使用期限Expiration
Redis允许你标记一个关键字的使用期限。你可以给予一个Unix时间戳形式自1970年1月1日起的绝对时间或者一个基于秒的存活时间。这是一个基于关键字的命令因此其不在乎关键字表示的是哪种类型的数据结构。
expire pages:about 30
expireat pages:about 1356933600
第一个命令将会在30秒后删除掉关键字包括其关联的值。第二个命令则会在2012年12月31日上午12点删除掉关键字。
这让Redis能成为一个理想的缓冲引擎。通过`ttl`命令,你可以知道一个关键字还能够存活多久。而通过`persist`命令,你可以把一个关键字的使用期限删除掉。
ttl pages:about
persist pages:about
最后,有个特殊的字符串命令,`setex`命令让你可以在一个单独的原子命令里设置一个字符串值,同时里指定一个生存期(这比任何事情都要方便)。
setex pages:about 30 '<h1>about us</h1>....'
### 发布和订阅Publication and Subscriptions
Redis的列表数据结构有`blpop`和`brpop`命令,能从列表里返回且删除第一个(或最后一个)元素,或者被堵塞,直到有一个元素可供操作。这可以用来实现一个简单的队列。
**(译注:对于`blpop`和`brpop`命令如果列表里没有关键字可供操作连接将被堵塞直到有另外的Redis客户端使用`lpush`或`rpush`命令推入关键字为止。)**
此外Redis对于消息发布和频道订阅有着一流的支持。你可以打开第二个`redis-cli`窗口,去尝试一下这些功能。在第一个窗口里订阅一个频道(我们会称它为`warnings`
subscribe warnings
其将会答复你订阅的信息。现在,在另一个窗口,发布一条消息到`warnings`频道:
publish warnings "it's over 9000!"
如果你回到第一个窗口,你应该已经接收到`warnings`频道发来的消息。
你可以订阅多个频道(`subscribe channel1 channel2 ...`),订阅一组基于模式的频道(`psubscribe warnings:*`),以及使用`unsubscribe`和`punsubscribe`命令停止监听一个或多个频道,或一个频道模式。
最后,可以注意到`publish`命令的返回值是1这指出了接收到消息的客户端数量。
### 监控和延迟日志Monitor and Slow Log
`monitor`命令可以让你查看Redis正在做什么。这是一个优秀的调试工具能让你了解你的程序如何与Redis进行交互。在两个`redis-cli`窗口中选一个(如果其中一个还处于订阅状态,你可以使用`unsubscribe`命令退订,或者直接关掉窗口再重新打开一个新窗口)键入`monitor`命令。在另一个窗口,执行任何其他类型的命令(例如`get`或`set`命令)。在第一个窗口里,你应该可以看到这些命令,包括他们的参数。
在实际生产环境里,你应该谨慎运行`monitor`命令,这真的仅仅就是一个很有用的调试和开发工具。除此之外,没有更多要说的了。
随同`monitor`命令一起Redis拥有一个`slowlog`命令,这是一个优秀的性能剖析工具。其会记录执行时间超过一定数量**微秒**的命令。在下一章节我们会简略地涉及如何配置Redis现在你可以按下面的输入配置Redis去记录所有的命令
config set slowlog-log-slower-than 0
然后,执行一些命令。最后,你可以检索到所有日志,或者检索最近的那些日志:
slowlog get
slowlog get 10
通过键入`slowlog len`,你可以获取延迟日志里的日志数量。
对于每个被你键入的命令你应该查看4个参数
* 一个自动递增的id
* 一个Unix时间戳表示命令开始运行的时间
* 一个微妙级的时间,显示命令运行的总时间
* 该命令以及所带参数
延迟日志保存在存储器中因此在生产环境中运行即使有一个低阀值也应该不是一个问题。默认情况下它将会追踪最近的1024个日志。
### 排序Sort
`sort`命令是Redis最强大的命令之一。它让你可以在一个列表、集合或者分类集合里对值进行排序分类集合是通过标记来进行排序而不是集合里的成员。下面是一个`sort`命令的简单用例:
rpush users:leto:guesses 5 9 10 2 4 10 19 2
sort users:leto:guesses
这将返回进行升序排序后的值。这里有一个更高级的例子:
sadd friends:ghanima leto paul chani jessica alia duncan
sort friends:ghanima limit 0 3 desc alpha
上面的命令向我们展示了,如何对已排序的记录进行分页(通过`limit`),如何返回降序排序的结果(通过`desc`),以及如何用字典序排序代替数值序排序(通过`alpha`)。
`sort`命令的真正力量是其基于引用对象来进行排序的能力。早先的时候我们说明了列表、集合和分类集合很常被用于引用其他的Redis对象`sort`命令能够解引用这些关系而且通过潜在值来进行排序。例如假设我们有一个Bug追踪器能让用户看到各类已存在问题。我们可能使用一个集合数据结构去追踪正在被监视的问题
sadd watch:leto 12339 1382 338 9338
你可能会有强烈的感觉想要通过id来排序这些问题默认的排序就是这样的但是我们更可能是通过问题的严重性来对这些问题进行排序。为此我们要告诉Redis将使用什么模式来进行排序。首先为了可以看到一个有意义的结果让我们添加多一点数据
set severity:12339 3
set severity:1382 2
set severity:338 5
set severity:9338 4
要通过问题的严重性来降序排序这些Bug你可以这样做
sort watch:leto by severity:* desc
Redis将会用存储在列表集合或分类集合中的值去替代模式中的`*`(通过`by`。这会创建出关键字名字Redis将通过查询其实际值来排序。
在Redis里虽然你可以有成千上万个关键字类似上面展示的关系还是会引起一些混乱。幸好`sort`命令也可以工作在散列数据结构及其相关域里。相对于拥有大量的高层次关键字,你可以利用散列:
hset bug:12339 severity 3
hset bug:12339 priority 1
hset bug:12339 details "{id: 12339, ....}"
hset bug:1382 severity 2
hset bug:1382 priority 2
hset bug:1382 details "{id: 1382, ....}"
hset bug:338 severity 5
hset bug:338 priority 3
hset bug:338 details "{id: 338, ....}"
hset bug:9338 severity 4
hset bug:9338 priority 2
hset bug:9338 details "{id: 9338, ....}"
所有的事情不仅变得更为容易管理,而且我们能通过`severity`或`priority`来进行排序,还可以告诉`sort`命令具体要检索出哪一个域的数据:
sort watch:leto by bug:*->priority get bug:*->details
相同的值替代出现了但Redis还能识别`->`符号,用它来查看散列中指定的域。里面还包括了`get`参数这里也会进行值替代和域查看从而检索出Bug的细节details域的数据
对于太大的集合,`sort`命令的执行可能会变得很慢。好消息是,`sort`命令的输出可以被存储起来:
sort watch:leto by bug:*->priority get bug:*->details store watch_by_priority:leto
使用我们已经看过的`expiration`命令,再结合`sort`命令的`store`能力,这是一个美妙的组合。
### 小结
这一章主要关注那些非特定数据结构关联的命令。和其他事情一样,它们的使用依情况而定。构建一个程序或特性时,可能不会用到使用期限、发布和订阅或者排序等功能。但知道这些功能的存在是很好的。而且,我们也只接触到了一些命令。还有更多的命令,当你消化理解完这本书后,非常值得去浏览一下[完整的命令列表](http://redis.io/commands)。
\clearpage
## 第5章 - 管理
在最后一章里我们将集中谈论Redis运行中的一些管理方面内容。这是一个不完整的Redis管理指南我们将会回答一些基本的问题初接触Redis的新用户可能会很感兴趣。
### 配置Configuration
当你第一次运行Redis的服务器它会向你显示一个警告指`redis.conf`文件没有被找到。这个文件可以被用来配置Redis的各个方面。一个充分定义well-documented的`redis.conf`文件对各个版本的Redis都有效。范例文件包含了默认的配置选项因此对于想要了解设置在干什么或默认设置是什么都会很有用。你可以在<https://github.com/antirez/redis/raw/2.4.6/redis.conf>找到这个文件。
**这个配置文件针对的是Redis 2.4.6你应该用你的版本号替代上面URL里的"2.4.6"。运行`info`命令其显示的第一个值就是Redis的版本号。**
因为这个文件已经是充分定义well-documented我们就不去再进行设置了。
除了通过`redis.conf`文件来配置Redis`config set`命令可以用来对个别值进行设置。实际上,在将`slowlog-log-slower-than`设置为0时我们就已经使用过这个命令了。
还有一个`config get`命令能显示一个设置值。这个命令支持模式匹配因此如果我们想要显示关联于日志logging的所有设置我们可以这样做
config get *log*
### 验证Authentication
通过设置`requirepass`(使用`config set`命令或`redis.conf`文件可以让Redis需要一个密码验证。当`requirepass`被设置了一个值(就是待用的密码),客户端将需要执行一个`auth password`命令。
一旦一个客户端通过了验证,就可以在任意数据库里执行任何一条命令,包括`flushall`命令,这将会清除掉每一个数据库里的所有关键字。通过配置,你可以重命名一些重要命令为混乱的字符串,从而获得一些安全性。
rename-command CONFIG 5ec4db169f9d4dddacbfb0c26ea7e5ef
rename-command FLUSHALL 1041285018a942a4922cbf76623b741e
或者,你可以将新名字设置为一个空字符串,从而禁用掉一个命令。
### 大小限制Size Limitations
当你开始使用Redis你可能会想知道我能使用多少个关键字还可能想知道一个散列数据结构能有多少个域尤其是当你用它来组织数据时或者是一个列表数据结构或集合数据结构能有多少个元素对于每一个实例实际限制都能达到亿万级别hundreds of millions
### 复制Replication
Redis支持复制功能这意味着当你向一个Redis实例Master进行写入时一个或多个其他实例Slaves能通过Master实例来保持更新。可以在配置文件里设置`slaveof`,或使用`slaveof`命令来配置一个Slave实例。对于那些没有进行这些设置的Redis实例就可能一个Master实例。
为了更好保护你的数据复制功能拷贝数据到不同的服务器。复制功能还能用于改善性能因为读取请求可以被发送到Slave实例。他们可能会返回一些稍微滞后的数据但对于大多数程序来说这是一个值得做的折衷。
遗憾的是Redis的复制功能还没有提供自动故障恢复。如果Master实例崩溃了一个Slave实例需要手动的进行升级。如果你想使用Redis去达到某种高可用性对于使用心跳监控heartbeat monitoring和脚本自动开关scripts to automate the switch的传统高可用性工具来说现在还是一个棘手的难题。
### 备份文件Backups
备份Redis非常简单你可以将Redis的快照snapshot拷贝到任何地方包括S3、FTP等。默认情况下Redis会把快照存储为一个名为`dump.rdb`的文件。在任何时候,你都可以对这个文件执行`scp`、`ftp`或`cp`等常用命令。
有一种常见情况在Master实例上会停用快照以及单一附加文件aof然后让一个Slave实例去处理备份事宜。这可以帮助减少Master实例的载荷。在不损害整体系统响应性的情况下你还可以在Slave实例上设置更多主动存储的参数。
### 缩放和Redis集群Scaling and Redis Cluster
复制功能Replication是一个成长中的网站可以利用的第一个工具。有一些命令会比另外一些来的昂贵例如`sort`命令将这些运行载荷转移到一个Slave实例里可以保持整体系统对于查询的快速响应。
此外通过分发你的关键字到多个Redis实例里可以达到真正的缩放Redis记住Redis是单线程的这些可以运行在同一个逻辑框里。随着时间的推移你将需要特别注意这些事情尽管许多的Redis载体都提供了consistent-hashing算法。对于数据水平分布horizontal distribution的考虑不在这本书所讨论的范围内。这些东西你也很可能不需要去担心但是无论你使用哪一种解决方案有一些事情你还是必须意识到。
好消息是这些工作都可在Redis集群下进行。不仅提供水平缩放包括均衡为了高可用性还提供了自动故障恢复。
高可用性和缩放是可以达到的只要你愿意为此付出时间和精力Redis集群也使事情变得简单多了。
### 小结
在过去的一段时间里已经有许多的计划和网站使用了Redis毫无疑问Redis已经可以应用于实际生产中了。然而一些工具还是不够成熟尤其是一些安全性和可用性相关的工具。对于Redis集群我们希望很快就能看到其实现这应该能为一些现有的管理挑战提供处理帮忙。
\clearpage
## 总结
在许多方面Redis体现了一种简易的数据处理方式其剥离掉了大部分的复杂性和抽象并可有效的在不同系统里运行。不少情况下选择Redis不是最佳的选择。在另一些情况里Redis就像是为你的数据提供了特别定制的解决方案。
最终回到我最开始所说的Redis很容易学习。现在有许多的新技术很难弄清楚哪些才真正值得我们花时间去学习。如果你从实际好处来考虑Redis提供了他的简单性。我坚信对于你和你的团队学习Redis是最好的技术投资之一。

View file

@ -30,8 +30,10 @@ import (
const slash = string(os.PathSeparator)
var DefaultTimer = nitro.Initalize()
type Site struct {
c Config
Config Config
Pages Pages
Tmpl *template.Template
Indexes IndexList
@ -56,15 +58,25 @@ func (s *Site) getFromIndex(kind string, name string) Pages {
return s.Indexes[kind][name]
}
func NewSite(config *Config) *Site {
return &Site{c: *config, timer: nitro.Initalize()}
func (s *Site) timerStep(step string) {
if s.timer == nil {
s.timer = DefaultTimer
}
s.timer.Step(step)
}
func (site *Site) Build() (err error) {
if err = site.Process(); err != nil {
return
}
site.Render()
if err = site.Render(); err != nil {
fmt.Printf("Error rendering site: %s\n", err)
fmt.Printf("Available templates:")
for _, template := range site.Tmpl.Templates() {
fmt.Printf("\t%s\n", template.Name())
}
return
}
site.Write()
return nil
}
@ -77,36 +89,39 @@ func (site *Site) Analyze() {
func (site *Site) Process() (err error) {
site.initialize()
site.prepTemplates()
site.timer.Step("initialize & template prep")
site.timerStep("initialize & template prep")
site.CreatePages()
site.setupPrevNext()
site.timer.Step("import pages")
site.timerStep("import pages")
if err = site.BuildSiteMeta(); err != nil {
return
}
site.timer.Step("build indexes")
site.timerStep("build indexes")
return
}
func (site *Site) Render() {
func (site *Site) Render() (err error) {
site.ProcessShortcodes()
site.timer.Step("render shortcodes")
site.timerStep("render shortcodes")
site.AbsUrlify()
site.timer.Step("absolute URLify")
site.timerStep("absolute URLify")
site.RenderIndexes()
site.RenderIndexesIndexes()
site.timer.Step("render and write indexes")
site.timerStep("render and write indexes")
site.RenderLists()
site.timer.Step("render and write lists")
site.RenderPages()
site.timer.Step("render pages")
site.timerStep("render and write lists")
if err = site.RenderPages(); err != nil {
return
}
site.timerStep("render pages")
site.RenderHomePage()
site.timer.Step("render and write homepage")
site.timerStep("render and write homepage")
return
}
func (site *Site) Write() {
site.WritePages()
site.timer.Step("write pages")
site.timerStep("write pages")
}
func (site *Site) checkDescriptions() {
@ -141,14 +156,14 @@ func (s *Site) prepTemplates() {
return err
}
text := string(filetext)
name := path[len(s.c.GetAbsPath(s.c.LayoutDir))+1:]
name := path[len(s.Config.GetAbsPath(s.Config.LayoutDir))+1:]
t := templates.New(name)
template.Must(t.Parse(text))
}
return nil
}
filepath.Walk(s.c.GetAbsPath(s.c.LayoutDir), walker)
filepath.Walk(s.Config.GetAbsPath(s.Config.LayoutDir), walker)
s.Tmpl = templates
}
@ -171,24 +186,35 @@ func (s *Site) initialize() {
site.Files = append(site.Files, path)
return nil
}
return nil
}
filepath.Walk(s.c.GetAbsPath(s.c.ContentDir), walker)
filepath.Walk(s.Config.GetAbsPath(s.Config.ContentDir), walker)
s.Info = SiteInfo{BaseUrl: template.URL(s.c.BaseUrl), Title: s.c.Title, Config: &s.c}
s.Info = SiteInfo{BaseUrl: template.URL(s.Config.BaseUrl), Title: s.Config.Title, Config: &s.Config}
s.Shortcodes = make(map[string]ShortcodeFunc)
}
func (s *Site) absLayoutDir() string {
return s.Config.GetAbsPath(s.Config.LayoutDir)
}
func (s *Site) absContentDir() string {
return s.Config.GetAbsPath(s.Config.ContentDir)
}
func (s *Site) absPublishDir() string {
return s.Config.GetAbsPath(s.Config.PublishDir)
}
func (s *Site) checkDirectories() {
if b, _ := dirExists(s.c.GetAbsPath(s.c.LayoutDir)); !b {
FatalErr("No layout directory found, expecting to find it at " + s.c.GetAbsPath(s.c.LayoutDir))
if b, _ := dirExists(s.absLayoutDir()); !b {
FatalErr("No layout directory found, expecting to find it at " + s.absLayoutDir())
}
if b, _ := dirExists(s.c.GetAbsPath(s.c.ContentDir)); !b {
FatalErr("No source directory found, expecting to find it at " + s.c.GetAbsPath(s.c.ContentDir))
if b, _ := dirExists(s.absContentDir()); !b {
FatalErr("No source directory found, expecting to find it at " + s.absContentDir())
}
mkdirIf(s.c.GetAbsPath(s.c.PublishDir))
mkdirIf(s.absPublishDir())
}
func (s *Site) ProcessShortcodes() {
@ -200,11 +226,11 @@ func (s *Site) ProcessShortcodes() {
func (s *Site) AbsUrlify() {
for i, _ := range s.Pages {
content := string(s.Pages[i].Content)
content = strings.Replace(content, " src=\"/", " src=\""+s.c.BaseUrl, -1)
content = strings.Replace(content, " src='/", " src='"+s.c.BaseUrl, -1)
content = strings.Replace(content, " href='/", " href='"+s.c.BaseUrl, -1)
content = strings.Replace(content, " href=\"/", " href=\""+s.c.BaseUrl, -1)
baseWithoutTrailingSlash := strings.TrimRight(s.c.BaseUrl, "/")
content = strings.Replace(content, " src=\"/", " src=\""+s.Config.BaseUrl+"/", -1)
content = strings.Replace(content, " src='/", " src='"+s.Config.BaseUrl+"/", -1)
content = strings.Replace(content, " href='/", " href='"+s.Config.BaseUrl+"/", -1)
content = strings.Replace(content, " href=\"/", " href=\""+s.Config.BaseUrl+"/", -1)
baseWithoutTrailingSlash := strings.TrimRight(s.Config.BaseUrl, "/")
content = strings.Replace(content, baseWithoutTrailingSlash+"//", baseWithoutTrailingSlash+"/", -1)
s.Pages[i].Content = template.HTML(content)
}
@ -216,7 +242,7 @@ func (s *Site) CreatePages() {
page.Site = s.Info
page.Tmpl = s.Tmpl
s.setOutFile(page)
if s.c.BuildDrafts || !page.Draft {
if s.Config.BuildDrafts || !page.Draft {
s.Pages = append(s.Pages, page)
}
}
@ -240,7 +266,7 @@ func (s *Site) BuildSiteMeta() (err error) {
s.Indexes = make(IndexList)
s.Sections = make(Index)
for _, plural := range s.c.Indexes {
for _, plural := range s.Config.Indexes {
s.Indexes[plural] = make(Index)
for i, p := range s.Pages {
vals := p.GetParam(plural)
@ -268,7 +294,7 @@ func (s *Site) BuildSiteMeta() (err error) {
s.Info.Indexes = s.Indexes.BuildOrderedIndexList()
if len(s.Pages) == 0 {
return errors.New(fmt.Sprintf("Unable to build site metadata, no pages found in directory %s", s.c.ContentDir))
return errors.New(fmt.Sprintf("Unable to build site metadata, no pages found in directory %s", s.Config.ContentDir))
}
s.Info.LastChange = s.Pages[0].Date
@ -280,10 +306,15 @@ func (s *Site) BuildSiteMeta() (err error) {
return
}
func (s *Site) RenderPages() {
func (s *Site) RenderPages() error {
for i, _ := range s.Pages {
s.Pages[i].RenderedContent = s.RenderThing(s.Pages[i], s.Pages[i].Layout())
content, err := s.RenderThing(s.Pages[i], s.Pages[i].Layout())
if err != nil {
return err
}
s.Pages[i].RenderedContent = content
}
return nil
}
func (s *Site) WritePages() {
@ -295,10 +326,10 @@ func (s *Site) WritePages() {
func (s *Site) setOutFile(p *Page) {
if len(strings.TrimSpace(p.Slug)) > 0 {
// Use Slug if provided
if s.c.UglyUrls {
if s.Config.UglyUrls {
p.OutFile = strings.TrimSpace(p.Slug + "." + p.Extension)
} else {
p.OutFile = strings.TrimSpace(p.Slug + "/index.html")
p.OutFile = strings.TrimSpace(p.Slug + slash + "index.html")
}
} else if len(strings.TrimSpace(p.Url)) > 2 {
// Use Url if provided & Slug missing
@ -306,23 +337,23 @@ func (s *Site) setOutFile(p *Page) {
} else {
// Fall back to filename
_, t := filepath.Split(p.FileName)
if s.c.UglyUrls {
if s.Config.UglyUrls {
p.OutFile = replaceExtension(strings.TrimSpace(t), p.Extension)
} else {
file, _ := fileExt(strings.TrimSpace(t))
p.OutFile = file + "/index." + p.Extension
p.OutFile = file + slash + "index." + p.Extension
}
}
}
func (s *Site) RenderIndexes() {
for singular, plural := range s.c.Indexes {
func (s *Site) RenderIndexes() error {
for singular, plural := range s.Config.Indexes {
for k, o := range s.Indexes[plural] {
n := s.NewNode()
n.Title = strings.Title(k)
url := Urlize(plural + "/" + k)
url := Urlize(plural + slash + k)
plink := url
if s.c.UglyUrls {
if s.Config.UglyUrls {
n.Url = url + ".html"
plink = n.Url
} else {
@ -334,14 +365,16 @@ func (s *Site) RenderIndexes() {
n.Data[singular] = o
n.Data["Pages"] = o
layout := "indexes" + slash + singular + ".html"
x := s.RenderThing(n, layout)
x, err := s.RenderThing(n, layout)
if err != nil {
return err
}
var base string
if s.c.UglyUrls {
base = plural + slash + k
if s.Config.UglyUrls {
base = plural + "/" + k
} else {
base = plural + slash + k + slash + "index"
base = plural + "/" + k + "/" + "index"
}
s.WritePublic(base+".html", x.Bytes())
@ -349,10 +382,10 @@ func (s *Site) RenderIndexes() {
if a := s.Tmpl.Lookup("rss.xml"); a != nil {
// XML Feed
y := s.NewXMLBuffer()
if s.c.UglyUrls {
if s.Config.UglyUrls {
n.Url = Urlize(plural + "/" + k + ".xml")
} else {
n.Url = Urlize(plural + "/" + k + "/index.xml")
n.Url = Urlize(plural + "/" + k + "/" + "index.xml")
}
n.Permalink = template.HTML(string(n.Site.BaseUrl) + n.Url)
s.Tmpl.ExecuteTemplate(y, "rss.xml", n)
@ -360,12 +393,13 @@ func (s *Site) RenderIndexes() {
}
}
}
return nil
}
func (s *Site) RenderIndexesIndexes() {
func (s *Site) RenderIndexesIndexes() (err error) {
layout := "indexes" + slash + "indexes.html"
if s.Tmpl.Lookup(layout) != nil {
for singular, plural := range s.c.Indexes {
for singular, plural := range s.Config.Indexes {
n := s.NewNode()
n.Title = strings.Title(plural)
url := Urlize(plural)
@ -376,24 +410,29 @@ func (s *Site) RenderIndexesIndexes() {
n.Data["Index"] = s.Indexes[plural]
n.Data["OrderedIndex"] = s.Info.Indexes[plural]
x := s.RenderThing(n, layout)
x, err := s.RenderThing(n, layout)
s.WritePublic(plural+slash+"index.html", x.Bytes())
return err
}
}
return
}
func (s *Site) RenderLists() {
func (s *Site) RenderLists() error {
for section, data := range s.Sections {
n := s.NewNode()
n.Title = strings.Title(inflect.Pluralize(section))
n.Url = Urlize(section + "/index.html")
n.Url = Urlize(section + "/" + "index.html")
n.Permalink = template.HTML(MakePermalink(string(n.Site.BaseUrl), string(n.Url)))
n.RSSlink = template.HTML(MakePermalink(string(n.Site.BaseUrl), string(section+".xml")))
n.Date = data[0].Date
n.Data["Pages"] = data
layout := "indexes/" + section + ".html"
layout := "indexes" + slash + section + ".html"
x := s.RenderThing(n, layout)
x, err := s.RenderThing(n, layout)
if err != nil {
return err
}
s.WritePublic(section+slash+"index.html", x.Bytes())
if a := s.Tmpl.Lookup("rss.xml"); a != nil {
@ -405,9 +444,10 @@ func (s *Site) RenderLists() {
s.WritePublic(section+slash+"index.xml", y.Bytes())
}
}
return nil
}
func (s *Site) RenderHomePage() {
func (s *Site) RenderHomePage() error {
n := s.NewNode()
n.Title = n.Site.Title
n.Url = Urlize(string(n.Site.BaseUrl))
@ -419,7 +459,10 @@ func (s *Site) RenderHomePage() {
} else {
n.Data["Pages"] = s.Pages[:9]
}
x := s.RenderThing(n, "index.html")
x, err := s.RenderThing(n, "index.html")
if err != nil {
return err
}
s.WritePublic("index.html", x.Bytes())
if a := s.Tmpl.Lookup("rss.xml"); a != nil {
@ -431,11 +474,12 @@ func (s *Site) RenderHomePage() {
s.Tmpl.ExecuteTemplate(y, "rss.xml", n)
s.WritePublic("index.xml", y.Bytes())
}
return nil
}
func (s *Site) Stats() {
fmt.Printf("%d pages created \n", len(s.Pages))
for _, pl := range s.c.Indexes {
for _, pl := range s.Config.Indexes {
fmt.Printf("%d %s created\n", len(s.Indexes[pl]), pl)
}
}
@ -448,10 +492,10 @@ func (s *Site) NewNode() Node {
return y
}
func (s *Site) RenderThing(d interface{}, layout string) *bytes.Buffer {
func (s *Site) RenderThing(d interface{}, layout string) (*bytes.Buffer, error) {
buffer := new(bytes.Buffer)
s.Tmpl.ExecuteTemplate(buffer, layout, d)
return buffer
err := s.Tmpl.ExecuteTemplate(buffer, layout, d)
return buffer, err
}
func (s *Site) NewXMLBuffer() *bytes.Buffer {
@ -461,13 +505,13 @@ func (s *Site) NewXMLBuffer() *bytes.Buffer {
func (s *Site) WritePublic(path string, content []byte) {
if s.c.Verbose {
if s.Config.Verbose {
fmt.Println(path)
}
path, filename := filepath.Split(path)
path = filepath.FromSlash(s.c.GetAbsPath(filepath.Join(s.c.PublishDir, path)))
path = filepath.FromSlash(s.Config.GetAbsPath(filepath.Join(s.Config.PublishDir, path)))
err := mkdirIf(path)
if err != nil {

View file

@ -102,7 +102,7 @@ func main() {
}
if *checkMode {
site := hugolib.NewSite(config)
site := hugolib.Site{Config: *config}
site.Analyze()
os.Exit(0)
}
@ -143,7 +143,7 @@ func serve(port string, config *hugolib.Config) {
func buildSite(config *hugolib.Config) (site *hugolib.Site, err error) {
startTime := time.Now()
site = hugolib.NewSite(config)
site = &hugolib.Site{Config: *config}
err = site.Build()
if err != nil {
return