者 | 喵叔
責編 | 劉靜
出品 | CSDN(ID:CSDNnews)
今天我利用這篇文章給大家講解一下 C# 中的序列化與反序列化。這兩個概念我們在開發中經常用到,但是我們絕大部分只用到了其中的一部分,剩下的部分很多開發人員并不清楚,伸著可以說是不知道。因此我希望通過這篇文章能讓各位對序列化和反序列化的知識有更進一步的掌握。廢話不多說開始進入正題。
什么是序列化/反序列化
在所有的開發語言中都存在序列化和反序列化這個概念,所謂的序列化就是把一個對象信息轉化為一個可以持久存儲的數據形式,經過轉化后就可以方便的保存和傳輸了,因此序列化主要用于平臺之間的通訊。由序列化我們可以反推出所謂的反序列化就是將持久存儲的數據還原為對象。
C# 中的序列化/反序列化
在 C# 中我們經常會對 JSON 和 XML 進行序列化和反序列化 ,但是還有存在一種序列化/反序列化,那就是將對象序列化為二進制文件,將會二進制文件反序列化為對象。下面我將會對這三種序列化和反序列化進行講解。
1. JSON
JSON 的英文全稱是 JavaScript Object Notation ,是一種輕量級的數據交換格式。完全獨立于語言的文本格式 易于人閱讀和編寫同時也易于機器解析和生成。JSON 是目前互聯網中主流的數據交換格式,同時也是很多開發語言配置文件的主流格式。
在 .NET 中存在兩個類對 JSON 進行處理,分別是 DataContractJsonSerializer 和 JavaScriptSerializer ,這兩個類的功能基本一致。DataContractJsonSerializer 位于命名空間 System.Runtime.Serialization.Json 下,它的特點是必須使用 DataContract 以及 DataMember 屬性標記成員。JavaScriptSerializer 位于命名空間 System.Web.Script.Serialization 下,通過名字和它所在的命名空間我們可以得知它主要用在網絡通信中,它可以序列化任何類型的對象。同樣 .NET 中也存在一個強大的第三方 JSON 序列化/反序列化庫 Newtonsoft.Json ,他比前兩個類用起來要方便很多。下面我們對這三個序列化/反序列化的方式分別進行講解。
DataContractJsonSerializer
首先我們需要在項目中引用 DataContractJsonSerializer 所在的命名空間,這里要注意的時我們不僅要在項目中添加引用 System.Runtime.Serialization 還需要添加引用 System.ServiceModel.Web 。將這兩個命名空添加到命名空間后就可以在代碼中引入 DataContractJsonSerializer 的命名空間了。
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
引入命名空間后我們開始編寫序列化類
[DataContract]
class Student
{
[DataMember]
public string Name { get; set; }
[DataMember]
public int Sex { get; set; }
[DataMember]
public int Age { get; set; }
[DataMember]
public Address Address { get; set; }
}
[DataContract]
class Address
{
[DataMember]
public string City { get; set; }
[DataMember]
public string Road { get; set; }
}
在上述代碼中我們看到在類的頭部添加了 DataContract 特性,以及在類的屬性上也增加了 DataMember 特性 。一旦一個類被聲明為 DataContract 時就代表著該類可以被序列化,并且可以在服務端和客戶端傳輸。只有聲明為DataContract的類型的對象可以被傳送,且只有成員屬性會被傳遞,成員方法不會被傳遞。默認情況下類中的所有成員屬性都不會被序列化傳輸出去,如果需要將成員數據傳輸出去就需要在屬性頭部加入 DataMember 。
下面我們就利用 DataContractJsonSerializer 對對象盡心序列化和反序列化,代碼如下
class Program
{
static void Main(string[] args)
{
#region 對象轉JSON字符串
Student student = new Student
{
Name = "Tom",
Age = 20,
Sex = 1,
Address = new Address
{
City = "NYC",
Road = "ABC"
}
};
//利用WriteObject方法序列化為 JSON
DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(Student));
MemoryStream stream = new MemoryStream;
serializer.WriteObject(stream, student);
byte bytes=new byte[stream.Length];
stream.Position = 0;
stream.Read(bytes, 0, (int)stream.Length);
string jsonStr = Encoding.UTF8.GetString(bytes);
Console.WriteLine(jsonStr);
#endregion
#region JSON字符串轉對象
stream = new MemoryStream(Encoding.Default.GetBytes(jsonStr));
student = (Student)serializer.ReadObject(stream);
Console.WriteLine("Name: " +student.Name);
Console.WriteLine("Sex: " + student.Sex);
Console.WriteLine("Age: " + student.Age);
Console.WriteLine("Address: " + student.Address.City+" "+student.Address.Road);
#endregion
Console.ReadLine;
}
}
輸出結果如下:
JavaScriptSerializer
我們利用前面定義的類,來看一下 JavaScriptSerializer 的使用方法,我們將前面定義的類中的 DataContract 和 DataMember 都去掉。我們如果要使用 JavaScriptSerializer 只需引入 System.Web.Script.Serialization 命名空間即可。代碼如下:
using System.Web.Script.Serialization;
下面我們就利用 JavaScriptSerializer 對象進行序列化和反序列化,代碼如下:
class Program
{
static void Main(string[] args)
{
#region 序列化
Student student = new Student
{
Name = "Tom",
Age = 20,
Sex = 1,
Address = new Address
{
City = "NYC",
Road = "ABC"
}
};
//初始化
JavaScriptSerializer serializer = new JavaScriptSerializer;
string jsonStr = serializer.Serialize(student);
Console.WriteLine(jsonStr);
#endregion
#region 反序列化
student = serializer.Deserialize<Student>(jsonStr);
Console.WriteLine("Name: " +student.Name);
Console.WriteLine("Sex: " + student.Sex);
Console.WriteLine("Age: " + student.Age);
Console.WriteLine("Address: " + student.Address.City+" "+student.Address.Road);
#endregion
Console.ReadLine;
}
}
從上面的代碼我們可以看出利用 JavaScriptSerializer 序列化和反序列化要比 DataContractJsonSerializer 類方便。上述代碼運行結果如下:
Newtonsoft.Json
Newtonsoft.Json 功能有很多,除了序列化反序列化之外,還有 Linq To Json、Json Path、 XML support等,我們這篇文章我們只講解其中的序列化和反序列化。使用 Newtonsoft.Json 前首先我們需要在 nuget 中搜索并安裝,安裝完成后引入 Newtonsoft.Json,代碼如下:
using Newtonsoft.Json;
下面我們來看看 Newtonsoft.Json 的具體使用:
class Program
{
static void Main(string[] args)
{
#region 序列化
Student student = new Student
{
Name = "Tom",
Age = 20,
Sex = 1,
Address = new Address
{
City = "NYC",
Road = "ABC"
}
};
string jsonStr = JsonConvert.SerializeObject(student);
Console.WriteLine(jsonStr);
#endregion
#region 反序列化
student = JsonConvert.DeserializeObject<Student>(jsonStr);
Console.WriteLine("Name: " +student.Name);
Console.WriteLine("Sex: " + student.Sex);
Console.WriteLine("Age: " + student.Age);
Console.WriteLine("Address: " + student.Address.City+" "+student.Address.Road);
#endregion
Console.ReadLine;
}
}
上述代碼輸出結果如下:
從代碼中我們看到 Newtonsoft.Json 序列化和反序列化更加簡單,簡單到只需要一行代碼就完成了序列化和反序列化。
2. XML
在 JSON 還沒出現之前,XML 是互聯網上常用的數據交換格式和規范。.NET 中提供 XmlSerializer 類將對象序列化為 XML 和將 XML 反序列化為對象,使用方法是首先實例化,然后調用序列化/反序列化方法。下面我們依然使用最開始定義的那個類,來看看 XmlSerializer 的使用。使用前我們需要引入 using System.Xml.Serialization 命名空間。
using System.Xml.Serialization;
具體序列化/反序列化方法如下:
class Program
{
static void Main(string[] args)
{
#region 序列化
Student student = new Student
{
Name = "Tom",
Age = 20,
Sex = 1,
Address = new Address
{
City = "NYC",
Road = "ABC"
}
};
XmlSerializer xmlSerializer = new XmlSerializer(typeof(Student));
using (FileStream stream = new FileStream(@"d:3.xml", FileMode.OpenOrCreate))
{
xmlSerializer.Serialize(stream, student);
}
#endregion
#region 反序列化
using (FileStream stream = new FileStream(@"d:3.xml", FileMode.OpenOrCreate))
{
XmlReader xmlReader = new XmlTextReader(stream);
student = xmlSerializer.Deserialize(xmlReader) as Student;
}
Console.WriteLine("Name: " + student.Name);
Console.WriteLine("Sex: " + student.Sex);
Console.WriteLine("Age: " + student.Age);
Console.WriteLine("Address: " + student.Address.City + " " + student.Address.Road);
#endregion
Console.ReadLine;
}
}
這里有一點需要注意,如果對象類的訪問修飾符不是 public 將會報錯。上述代碼運行結果如下:
3. 二進制
序列化為二進制,在實際開發中真的很少用到,但是我覺得還是有必要講一講,它的使用方法和 XmlSerializer 序列化/反序列化類似,首先實例化,然后調用序列化/反序列化方法。在進行序列化/反序列化前首先引入命名空間 System.Runtime.Serialization.Formatters.Binary ,同時修改對象類如下:
[Serializable]
public class Student
{
public string Name { get; set; }
public int Sex { get; set; }
public int Age { get; set; }
public Address Address { get; set; }
}
[Serializable]
public class Address
{
public string City { get; set; }
public string Road { get; set; }
}
上述代碼中我們在類的頭部加入了 Serializable 特性,這代表著整個類對象都需要序列化,如果我們不需要序列化其中某個屬性的話只需在該屬性上加上 NonSerialized 特性即可。下面我們來看一下序列化和反序列化的代碼:
class Program
{
static void Main(string[] args)
{
#region 序列化
Student student = new Student
{
Name = "Tom",
Age = 20,
Sex = 1,
Address = new Address
{
City = "NYC",
Road = "ABC"
}
};
BinaryFormatter binFormat = new BinaryFormatter;
string fileName = Path.Combine(@"D:\", @"321.txt");
using (Stream fStream = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite))
{
binFormat.Serialize(fStream, student);
}
#endregion
#region 反序列化
using (Stream fStream = new FileStream(fileName, FileMode.Open, FileAccess.ReadWrite))
{
fStream.Position = 0;
student = (Student)binFormat.Deserialize(fStream);
}
Console.WriteLine("Name: " + student.Name);
Console.WriteLine("Sex: " + student.Sex);
Console.WriteLine("Age: " + student.Age);
Console.WriteLine("Address: " + student.Address.City + " " + student.Address.Road);
#endregion
Console.ReadLine;
}
}
上述代碼最終輸出結果如下:
總結
這篇文章詳細講解了.NET中序列化和反序列化相關知識的使用,序列化和反序列化相關的只是還有很多,這里所講解的都是開發中經常用到的,也是面試過程中會提及的,因此大家需要牢記。
作者簡介:朱鋼,筆名喵叔,CSDN博客專家,.NET高級開發工程師,7年一線開發經驗,參與過電子政務系統和AI客服系統的開發,以及互聯網招聘網站的架構設計,目前就職于北京恒創融慧科技發展有限公司,從事企業級安全監控系統的開發。
聲明:本文系作者獨立觀點,不代表CSDN立場。
【END】
會如何安裝和運行Redis,并了解Redis的基礎知識后,本章將詳細介紹Redis的5種主要數據類型及相應的命令,帶領讀者真正進入Redis的世界。在學習的時候,手邊打開一個redis-cli程序來跟著一起輸入命令將會極大地提高學習效率。盡管在目前多數公司和團隊的Redis的應用是以緩存和隊列為主。
在之后的章節中你會遇到兩個學習伙伴:小白和宋老師。小白是一個標準的極客,最近剛開始他的Redis學習之旅,而他大學時的計算機老師宋老師恰好對Redis頗有研究,于是就順理成章地成為了小白的私人Redis教師。這不,小白想基于Redis開發一個博客,于是找到宋老師,向他請教。在本章中宋老師會向小白介紹Redis最核心的內容—數據類型,從他們的對話中你一定能學到不少知識!
3.2節到3.6節這5節將分別介紹Redis的5種數據類型,其中每節都是由4個部分組成,依次是“介紹”、“命令”、“實踐”和“命令拾遺”。“介紹”部分是對數據類型的概述,“命令”部分會對“實踐”部分將用到的命令進行介紹,“實踐”部分會講解該數據類型在開發中的應用方法,“命令拾遺”部分會對該數據類型其他比較有用的命令進行補充介紹。
在介紹Redis的數據類型之前,我們先來了解幾個比較基礎的命令作為熱身,趕快打開redis-cli,跟著樣例親自輸入命令來體驗一下吧!
KEYS pattern
pattern支持glob風格通配符格式,具體規則如表3-1所示。
表3-1 glob風格通配符規則
符 號含 義
? 匹配一個字符
* 匹配任意個(包括0個)字符
[] 匹配括號間的任一字符,可以使用“-”符號表示一個范圍,如a[b-d]可以匹配“ab”、“ac”和“ad”
\x 匹配字符x,用于轉義符號。如要匹配“?”就需要使用\?
現在Redis中空空如也(如果你從第2章開始就一直跟著本書的進度輸入命令,此時數據庫中可能還會有個foo鍵),為了演示KEYS命令,首先我們得給Redis加點料。使用SET命令(會在3.2節介紹)建立一個名為bar的鍵:
redis> SET bar 1
OK
然后使用KEYS *就能獲得Redis中所有的鍵了(當然由于數據庫中只有一個bar鍵,所以KEYS ba*或者KEYS bar等命令都能獲得同樣的結果):
redis> KEYS *
1) "bar"
注意
KEYS命令需要遍歷Redis中的所有鍵,當鍵的數量較多時會影響性能,不建議在生產環境中使用。
提示
Redis不區分命令大小寫,但在本書中均會使用大寫字母表示Redis命令。
EXISTS key
如果鍵存在則返回整數類型1,否則返回0。例如:
redis> EXISTS bar
(integer) 1
redis> EXISTS noexists
(integer) 0
DEL key [key …]
可以刪除一個或多個鍵,返回值是刪除的鍵的個數。例如:
redis> DEL bar
(integer) 1
redis> DEL bar
(integer) 0
第二次執行DEL命令時因為bar鍵已經被刪除了,實際上并沒有刪除任何鍵,所以返回0。
技巧
DEL命令的參數不支持通配符,但我們可以結合Linux的管道和xargs命令自己實現刪除所有符合規則的鍵。比如要刪除所有以“user:”開頭的鍵,就可以執行redis-cli KEYS "user:*"|xargs redis-cli DEL。
另外由于DEL命令支持多個鍵作為參數,所以還可以執行redis-cli DEL``redis-cli KEYS "user:*" 來達到同樣的效果,但是性能更好。
TYPE key
TYPE命令用來獲得鍵值的數據類型,返回值可能是string(字符串類型)、hash(散列類型)、list(列表類型)、set(集合類型)、zset(有序集合類型)。例如:
redis> SET foo 1
OK
redis> TYPE foo
string
redis> LPUSH bar 1
(integer) 1
redis> TYPE bar
list
LPUSH命令的作用是向指定的列表類型鍵中增加一個元素,如果鍵不存在則創建它,3.4節會詳細介紹。
作為一個愛造輪子的資深極客,小白每次看到自己博客最下面的“Powered by WordPress”{![即“由WordPress驅動”。WordPress是一個開源的博客程序,用戶可以借其通過簡單的配置搭建一個博客或內容管理系統。]}都覺得有些不舒服,終于有一天他下定決心要開發一個屬于自己的博客。但是用膩了MySQL數據庫的小白總想嘗試一下新技術,恰好上次參加Node Party時聽人介紹過Redis數據庫,便想著趁機試一試。可小白只知道Redis是一個鍵值對數據庫,其他的一概不知。抱著試一試的態度,小白找到了自己大學時教計算機的宋老師,一問之下欣喜地發現宋老師竟然對Redis頗有研究。宋老師有感于小白的好學,決定給小白開個小灶。
小白:
宋老師您好,我最近聽別人介紹過Redis,當時就對它很感興趣。恰好最近想開發一個博客,準備嘗試一下它。有什么能快速學會Redis的方法嗎?
宋老師笑著說:
心急吃不了熱豆腐,要學會Redis就要先掌握Redis的鍵值數據類型和相關的命令,這些內容是Redis的基礎。為了讓你更全面地了解Redis的每種數據類型,接下來我會先講解如何將Redis作為數據庫使用,但是實際上Redis可不只是數據庫這么簡單,更多的公司和團隊將Redis用作緩存和隊列系統,而這部分內容等你掌握了Redis的基礎后我會再進行介紹。作為開始,我先來講講Redis中最基本的數據類型—字符串類型。
字符串類型是Redis中最基本的數據類型,它能存儲任何形式的字符串,包括二進制數據。你可以用其存儲用戶的郵箱、JSON 化的對象甚至是一張圖片。一個字符串類型鍵允許存儲的數據的最大容量是512 MB{![ Redis的作者考慮過讓字符串類型鍵支持超過 512 MB大小的數據,未來的版本也可能會放寬這一限制,但無論如何,考慮到Redis的數據是使用內存存儲的,512 MB的限制已經非常寬松了。]}。
字符串類型是其他4種數據類型的基礎,其他數據類型和字符串類型的差別從某種角度來說只是組織字符串的形式不同。例如,列表類型是以列表的形式組織字符串,而集合類型是以集合的形式組織字符串。學習過本章后面幾節后相信讀者對此會有更深的理解。
SET key value
GET key
SET和GET是Redis中最簡單的兩個命令,它們實現的功能和編程語言中的讀寫變量相似,如key = "hello"在Redis中是這樣表示的:
redis> SET key hello
OK
想要讀取鍵值則更簡單:
redis> GET key
"hello"
當鍵不存在時會返回空結果。
為了節約篇幅,同時避免讀者過早地被編程語言的細節困擾,本書大部分章節將只使用redis-cli進行命令演示(必要的時候會配合偽代碼),第5章會專門介紹在各種編程語言(PHP、Python、Ruby和Node.js)中使用Redis的方法。
不過,為了能讓讀者提前對Redis命令在實際開發時的用法有一個直觀的體會,這里會先使用PHP實現一個SET/GET命令的示例網頁:用戶訪問示例網頁時程序會通過GET命令判斷Redis中是否存儲了用戶的姓名,如果有則直接將姓名顯示出來(如圖3-1所示),如果沒有則會提示用戶填寫(如圖3-2所示),用戶單擊“提交”按鈕后程序會使用SET命令將用戶的姓名存入到Redis中。
3-2{441}
圖3-1 設置過姓名時的頁面
3-1{440}
圖3-2 沒有設置過姓名時的頁面
代碼如下:
<?php
// 加載Predis庫的自動加載函數
require './predis/autoload.php';
// 連接Redis
$redis= new Predis\Client(array(
'host' => '127.0.0.1',
'port' => 6379
));
// 如果提交了姓名則使用SET命令將姓名寫入到Redis中
if ($_GET['name']) {
$redis->set('name', $_GET['name']);
}
// 通過GET命令從Redis中讀取姓名
$name = $redis->get('name');
?><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>我的第一個Redis程序</title>
</head>
<body>
<?php if ($name): ?>
<p>您的姓名是:<?php echo $name; ?></p>
<?php else: ?>
<p>您還沒有設置姓名。</p>
<?php endif; ?>
<hr />
<h1>更改姓名</h1>
<form>
<p>
<label for="name">您的姓名:</label>
<input type="text" name="name" id="name" />
</p>
<p>
<button type="submit">提交</button>
</p>
</form>
</body>
</html>
在這個例子中我們使用PHP的Redis客戶端庫Predis與Redis通信。5.1節會專門介紹Predis,有興趣的讀者可以先跳到5.1節查看Predis的安裝方法來實際運行這個例子。
Redis的其他命令也可以使用Predis通過同樣的方式調用,如馬上要介紹的INCR命令的調用方法是$redis->incr(鍵名)。
INCR key前面說過字符串類型可以存儲任何形式的字符串,當存儲的字符串是整數形式時,Redis提供了一個實用的命令`INCR`,其作用是讓當前鍵值遞增,并返回遞增后的值,用法為:
redis> INCR num
(integer) 1
redis> INCR num
(integer) 2
當要操作的鍵不存在時會默認鍵值為0,所以第一次遞增后的結果是1。當鍵值不是整數時Redis會提示錯誤:
redis> SET foo lorem
OK
redis> INCR foo
(error) ERR value is not an integer or out of range
有些讀者會想到可以借助`GET`和`SET`兩個命令自己實現`incr`函數,偽代碼如下:
def incr($key)
$value = GET $key
if not $value
$value = 0
$value = $value + 1
SET $key, $value
return $value
如果Redis同時只連接了一個客戶端,那么上面的代碼沒有任何問題(其實還沒有加入錯誤處理,不過這并不是此處討論的重點)。可當同一時間有多個客戶端連接到Redis時則有可能出現競態條件(race condition){![競態條件是指一個系統或者進程的輸出,依賴于不受控制的事件的出現順序或者出現時機。]}。例如有兩個客戶端A和B都要執行我們自己實現的incr函數并準備將同一個鍵的鍵值遞增,當它們恰好同時執行到代碼第二行時二者讀取到的鍵值是一樣的,如“5”,而后它們各自將該值遞增到“6”并使用SET命令將其賦給原鍵,結果雖然對鍵執行了兩次遞增操作,最終的鍵值卻是“6”而不是預想中的“7”。包括INCR在內的所有Redis命令都是原子操作(atomic operation){![原子操作取“原子”的“不可拆分”的意思,原子操作是最小的執行單位,不會在執行的過程中被其他命令插入打斷。]},無論多少個客戶端同時連接,都不會出現上述情況。之后我們還會介紹利用事務(4.1節)和腳本(第6章)實現自定義的原子操作的方法。
博客的一個常見的功能是統計文章的訪問量,我們可以為每篇文章使用一個名為post:文章ID:page.view的鍵來記錄文章的訪問量,每次訪問文章的時候使用INCR命令使相應的鍵值遞增。
提示
Redis對于鍵的命名并沒有強制的要求,但比較好的實踐是用“對象類型:對象ID:對象屬性”來命名一個鍵,如使用鍵user:1:friends來存儲ID為1的用戶的好友列表。對于多個單詞則推薦使用“.”分隔,一方面是沿用以前的習慣(Redis以前版本的鍵名不能包含空格等特殊字符),另一方面是在redis-cli中容易輸入,無需使用雙引號包裹。另外為了日后維護方便,鍵的命名一定要有意義,如u:1:f的可讀性顯然不如user:1:friends好(雖然采用較短的名稱可以節省存儲空間,但由于鍵值的長度往往遠遠大于鍵名的長度,所以這部分的節省大部分情況下并不如可讀性來得重要)。
那么怎么為每篇文章生成一個唯一ID呢?在關系數據庫中我們通過設置字段屬性為AUTO_INCREMENT來實現每增加一條記錄自動為其生成一個唯一的遞增ID的目的,而在Redis中可以通過另一種模式來實現:對于每一類對象使用名為對象類型(復數形式):count{![這個鍵名只是參考命名,實際應用中可以使用任何容易理解的名稱。]}的鍵(如users:count)來存儲當前類型對象的數量,每增加一個新對象時都使用INCR命令遞增該鍵的值。由于使用INCR命令建立的鍵的初始鍵值是1,所以可以很容易得知,INCR命令的返回值既是加入該對象后的當前類型的對象總數,又是該新增對象的ID。
由于每個字符串類型鍵只能存儲一個字符串,而一篇博客文章是由標題、正文、作者與發布時間等多個元素構成的。為了存儲這些元素,我們需要使用序列化函數(如PHP中的serialize和JavaScript中的JSON.stringify)將它們轉換成一個字符串。除此之外因為字符串類型鍵可以存儲二進制數據,所以也可以使用MessagePack進行序列化,速度更快,占用空間也更小。
至此我們已經可以寫出發布新文章時與Redis操作相關的偽代碼首先獲得新文章的ID
$postID = INCR posts:count
alert("Hello CSDN");
將博客文章的諸多元素序列化成字符串
$serializedPost = serialize($title, $content, $author, $time)
把序列化后的字符串存一個入字符串類型的鍵中
SET post:$postID:data, $serializedPost文章數據的偽代碼如下(以訪問ID為42的文章為例):
從Redis中讀取文章數據
$serializedPost = GET post:42:data
將文章數據反序列化成文章的各個元素
$title, $content, $author, $time = unserialize($serializedPost)
獲取并遞增文章的訪問數量
$count = INCR post:42:page.view
除了使用序列化函數將文章的多個元素存入一個字符串類型鍵中外,還可以對每個元素使用一個字符串類型鍵來存儲,這種方法會在3.3.3節討論。
INCRBY key increment
alert("Hello CSDN");
`INCRBY`命令與`INCR`命令基本一樣,只不過前者可以通過`increment`參數指定一次增加的數值,如:
redis> INCRBY bar 2
(integer) 2
redis> INCRBY bar 3
(integer) 5
alert("Hello CSDN");
DECR key
DECRBY key decrement
DECR命令與INCR命令用法相同,只不過是讓鍵值遞減,例如:
redis> DECR bar
(integer) 4
而DECRBY命令的作用不用介紹想必讀者就可以猜到,DECRBY key 5相當于INCRBY key –5。
INCRBYFLOAT key increment
INCRBYFLOAT命令類似INCRBY命令,差別是前者可以遞增一個雙精度浮點數,如:
redis> INCRBYFLOAT bar 2.7
"6.7"
redis> INCRBYFLOAT bar 5E+4
"50006.69999999999999929"
APPEND key value
APPEND作用是向鍵值的末尾追加value。如果鍵不存在則將該鍵的值設置為value,即相當于SET key value。返回值是追加后字符串的總長度。如:
redis> SET key hello
OK
redis> APPEND key " world!"
(integer) 12
此時key的值是"hello world!"。APPEND命令的第二個參數加了雙引號,原因是該參數包含空格,在redis-cli中輸入需要雙引號以示區分。
STRLEN key
STRLEN命令返回鍵值的長度,如果鍵不存在則返回0。例如:
redis> STRLEN key
(integer) 12
redis> SET key 你好
OK
redis> STRLEN key
(integer) 6
前面提到了字符串類型可以存儲二進制數據,所以它可以存儲任何編碼的字符串。例子中Redis接收到的是使用UTF-8編碼的中文,由于“你”和“好”兩個字的UTF-8編碼的長度都是3,所以此例中會返回6。
MGET key [key …]
MSET key value [key value …]
MGET/MSET與GET/SET相似,不過MGET/MSET可以同時獲得/設置多個鍵的鍵值。例如:
redis> MSET key1 v1 key2 v2 key3 v3
OK
redis> GET key2
"v2"
redis> MGET key1 key3
1) "v1"
2) "v3"
GETBIT key offset
SETBIT key offset value
BITCOUNT key [start] [end]
BITOP operation destkey key [key …]
一個字節由8個二進制位組成,Redis提供了4個命令可以直接對二進制位進行操作。為了演示,我們首先將foo鍵賦值為bar:
redis> SET foo bar
OK
bar的3個字母“b”“a”和“r”對應的ASCII碼分別為98、97和114,轉換成二進制后分別為1100010、1100001和1110010,所以foo鍵中的二進制位結構如圖3-3所示。
3-3{513}
圖3-3 bar的二進制存儲結構
GETBIT命令可以獲得一個字符串類型鍵指定位置的二進制位的值(0或1),索引從0開始:
redis> GETBIT foo 0
(integer) 0
redis> GETBIT foo 6
(integer) 1
如果需要獲取的二進制位的索引超出了鍵值的二進制位的實際長度則默認位值是0:
redis> GETBIT foo 100000
(integer) 0
SETBIT命令可以設置字符串類型鍵指定位置的二進制位的值,返回值是該位置的舊值。如我們要將foo鍵值設置為aar,可以通過位操作將foo鍵的二進制位的索引第6位設為0,第7位設為1:
redis> SETBIT foo 6 0
(integer) 1
redis> SETBIT foo 7 1
(integer) 0
redis> GET foo
"aar"
如果要設置的位置超過了鍵值的二進制位的長度,SETBIT命令會自動將中間的二進制位設置為0,同理設置一個不存在的鍵的指定二進制位的值會自動將其前面的位賦值為0:
redis> SETBIT nofoo 10 1
(integer) 0
redis> GETBIT nofoo 5
(integer) 0
BITCOUNT命令可以獲得字符串類型鍵中值是1的二進制位個數,例如:
redis> BITCOUNT foo
(integer) 10
可以通過參數來限制統計的字節范圍,如我們只希望統計前兩個字節(即"aa"):
redis> BITCOUNT foo 0 1
(integer) 6
BITOP命令可以對多個字符串類型鍵進行位運算,并將結果存儲在destkey參數指定的鍵中。BITOP命令支持的運算操作有AND、OR、XOR和NOT。如我們可以對bar和aar進行OR運算:
redis> SET foo1 bar
OK
redis> SET foo2 aar
OK
redis> BITOP OR res foo1 foo2
(integer) 3
redis> GET res
"car"
運算過程如圖3-4所示。
3-4
{-:-}圖3-4 OR運算過程示意
Redis 2.8.7引入了BITPOS命令,可以獲得指定鍵的第一個位值是0或者1的位置。還是以“bar”這個鍵值為例,如果想獲取鍵值中的第一個二進制位為1的偏移量,則可以執行:
redis> SET foo bar
OK
redis> BITPOS foo 1
(integer) 1
結合圖3-3可以看出,正如BITPOS命令的結果所示,“bar”中的第一個值為1的二進制位的偏移量為1(同其他命令一樣,BITPOS命令的索引也是從0開始算起)。那么有沒有可能指定二進制位的查詢范圍呢?BITPOS命令的第二個和第三個參數分別可以用來指定要查詢的起始字節(同樣從0開始算起)和結束字節。注意這里的單位不再是二進制位,而是字節。如果我們想查詢第二個字節到第三個字節之間(即“a”和“r”)出現的第一個值為1的二進制位的偏移量,則可以執行:
redis> BITPOS foo 1 1 2
(integer) 9
這里的返回結果的偏移量是從頭開始算起的,與起始字節無關。另外要特別說明的一個有趣的現象是如果不設置結束字節且鍵值的所有二進制位都是1,則當要查詢值為0的二進制位偏移量時,返回結果會是鍵值長度的下一個字位的偏移量。這是因為Redis會認為鍵值長度之后的二進制位都是0。
利用位操作命令可以非常緊湊地存儲布爾值。比如如果網站的每個用戶都有一個遞增的整數ID,如果使用一個字符串類型鍵配合位操作來記錄每個用戶的性別(用戶ID作為索引,二進制位值1和0表示男性和女性),那么記錄100萬個用戶的性別只需占用100 KB多的空間,而且由于GETBIT和SETBIT的時間復雜度都是O(1),所以讀取二進制位值性能很高。
注意
使用SETBIT命令時,如果當前鍵的鍵值長度小于要設置的二進制位的偏移量時,Redis會自動分配內存并將鍵值的當前長度到指定的偏移量之間的二進制位都設置為0。如果要分配的內存過大,則很可能會造成服務器的暫時阻塞而無法接收同一時間的其他請求。舉例而言,在一臺2014年的MacBook Pro筆記本上,設置偏移量232-1的值(即分配500 MB的內存)需要耗費將近1秒的時間。分配過大的偏移量除了會造成服務器阻塞,還會造成空間浪費。還是舉剛才存儲網站用戶性別的例子,如果這個網站的用戶ID是從100000001開始的,那么會造成10多MB的浪費,正確的做法是給每個用戶的ID減去100000000再進行存儲。
小白只用了半個多小時就把訪問統計和發表文章兩個部分做好了。同時借助Bootstrap框架,老師花了一小會兒時間教會了之前只涉獵過HTML的小白如何做出一個像樣的網頁界面。
接著小白發問:
接下來我想要做的功能是博客的文章列表頁,我設想在列表頁中每個文章只顯示標題部分,可是使用您剛才介紹的方法,若想取得文章的標題,必須把整個文章數據字符串取出來反序列化,而其中占用空間最大的文章內容部分卻是不需要的,這樣難道不會在傳輸和處理時造成資源浪費嗎?
老師有些驚喜地看著小白答道:“很對!”同時以一個夸張的幅度點了下頭,接著說:
這正是我接下來準備講的。不僅取數據時會有資源浪費,在修改數據時也會有這個問題,比如當你只想更改文章的標題時也不得不把整個文章數據字符串更新一遍。
沒等小白再問,老師就又繼續說道:
前面我說過Redis的強大特性之一就是提供了多種實用的數據類型,其中的散列類型可以非常好地解決這個問題。
我們現在已經知道Redis是采用字典結構以鍵值對的形式存儲數據的,而散列類型(hash)的鍵值也是一種字典結構,其存儲了字段(field)和字段值的映射,但字段值只能是字符串,不支持其他數據類型,換句話說,散列類型不能嵌套其他的數據類型。一個散列類型鍵可以包含至多232?1個字段。
提示
除了散列類型,Redis 的其他數據類型同樣不支持數據類型嵌套。比如集合類型的每個元素都只能是字符串,不能是另一個集合或散列表等。
散列類型適合存儲對象:使用對象類別和ID構成鍵名,使用字段表示對象的屬性,而字段值則存儲屬性值。例如要存儲ID為2的汽車對象,可以分別使用名為color、name和price的3個字段來存儲該輛汽車的顏色、名稱和價格。存儲結構如圖3-5所示。
3-5{480}
圖3-5 使用散列類型存儲汽車對象的結構圖
回想在關系數據庫中如果要存儲汽車對象,存儲結構如表3-2所示。
表3-2 關系數據庫存儲汽車資料的表結構
IDcolornameprice
1黑色寶馬100萬
2白色奧迪90萬
3藍色賓利600萬
數據是以二維表的形式存儲的,這就要求所有的記錄都擁有同樣的屬性,無法單獨為某條記錄增減屬性。如果想為ID為1的汽車增加生產日期屬性,就需要把數據表更改為如表3-3所示的結構。
表3-3 為其中一輛汽車增加一個“屬性”
IDcolornamepricedate
1黑色寶馬100萬2012年12月21日
2白色奧迪90萬
3藍色賓利600萬
對于ID為2和3的兩條記錄而言date字段是冗余的。可想而知當不同的記錄需要不同的屬性時,表的字段數量會越來越多以至于難以維護。而且當使用ORM{![即Object-Relational Mapping(對象關系映射)。]}將關系數據庫中的對象實體映射成程序中的實體時,修改表的結構往往意味著要中斷服務(重啟網站程序)。為了防止這些問題,在關系數據庫中存儲這種半結構化數據還需要額外的表才行。
而Redis的散列類型則不存在這個問題。雖然我們在圖3-5中描述了汽車對象的存儲結構,但是這個結構只是人為的約定,Redis并不要求每個鍵都依據此結構存儲,我們完全可以自由地為任何鍵增減字段而不影響其他鍵。
HSET key field value
HGET key field
HMSET key field value [field value …]
HMGET key field [field …]
HGETALL key
HSET命令用來給字段賦值,而HGET命令用來獲得字段的值。用法如下:
redis> HSET car price 500
(integer) 1
redis> HSET car name BMW
(integer) 1
redis> HGET car name
"BMW"
HSET命令的方便之處在于不區分插入和更新操作,這意味著修改數據時不用事先判斷字段是否存在來決定要執行的是插入操作(update)還是更新操作(insert)。當執行的是插入操作時(即之前字段不存在)HSET命令會返回1,當執行的是更新操作時(即之前字段已經存在)HSET命令會返回0。更進一步,當鍵本身不存在時,HSET命令還會自動建立它。
提示
在Redis中每個鍵都屬于一個明確的數據類型,如通過HSET命令建立的鍵是散列類型,通過SET命令建立的鍵是字符串類型等等。使用一種數據類型的命令操作另一種數據類型的鍵會提示錯誤:"ERR Operation against a key holding the wrong kind of value"{![并不是所有命令都是如此,比如SET命令可以覆蓋已經存在的鍵而不論原來鍵是什么類型。]}。
當需要同時設置多個字段的值時,可以使用HMSET命令。例如,下面兩條語句
HSET key field1 value1
HSET key field2 value2
可以用HMSET命令改寫成
HMSET key field1 value1 field2 value2
相應地,HMGET命令可以同時獲得多個字段的值:
redis> HMGET car price name
1) "500"
2) "BMW"
如果想獲取鍵中所有字段和字段值卻不知道鍵中有哪些字段時(如3.3.1節介紹的存儲汽車對象的例子,每個對象擁有的屬性都未必相同)應該使用HGETALL命令。如:
redis> HGETALL car
1) "price"
2) "500"
3) "name"
4) "BMW"
返回的結果是字段和字段值組成的列表,不是很直觀,好在很多語言的Redis客戶端會將 HGETALL的返回結果封裝成編程語言中的對象,處理起來就非常方便了。例如,在Node.js中:
redis.hgetall("car", function (error, car) {
// hgetall方法的返回的值被封裝成了JavaScript的對象
console.log(car.price);
console.log(car.name);
});
HEXISTS key field
HEXISTS命令用來判斷一個字段是否存在。如果存在則返回1,否則返回0(如果鍵不存在也會返回0)。
redis> HEXISTS car model
(integer) 0
redis> HSET car model C200
(integer) 1
redis> HEXISTS car model
(integer) 1
HSETNX key field value
HSETNX{![HSETNX中的“NX”表示“if Not eXists”(如果不存在)。]}命令與HSET命令類似,區別在于如果字段已經存在,HSETNX命令將不執行任何操作。其實現可以表示為如下偽代碼:
def hsetnx($key, $field, $value)
$isExists = HEXISTS $key, $field
if $isExists is 0
HSET $key, $field, $value
return 1
else
return 0
只不過HSETNX命令是原子操作,不用擔心競態條件。
HINCRBY key field increment
上一節的命令拾遺部分介紹了字符串類型的命令INCRBY,HINCRBY命令與之類似,可以使字段值增加指定的整數。散列類型沒有HINCR命令,但是可以通過HINCRBY key field 1來實現。
HINCRBY命令的示例如下:
redis> HINCRBY person score 60
(integer) 60
之前person鍵不存在,HINCRBY命令會自動建立該鍵并默認score字段在執行命令前的值為“0”。命令的返回值是增值后的字段值。
HDEL key field [field …]
HDEL命令可以刪除一個或多個字段,返回值是被刪除的字段個數:
redis> HDEL car price
(integer) 1
redis> HDEL car price
(integer) 0
3.2.3節介紹了可以將文章對象序列化后使用一個字符串類型鍵存儲,可是這種方法無法提供對單個字段的原子讀寫操作支持,從而產生競態條件,如兩個客戶端同時獲得并反序列化某個文章的數據,然后分別修改不同的屬性后存入,顯然后存入的數據會覆蓋之前的數據,最后只會有一個屬性被修改。另外如小白所說,即使只需要文章標題,程序也不得不將包括文章內容在內的所有文章數據取出并反序列化,比較消耗資源。
除此之外,還有一種方法是組合使用多個字符串類型鍵來存儲一篇文章的數據,如圖3-6所示。
3-6{300}
{-:-}圖3-6 使用多個字符串類型鍵存儲一個對象
使用這種方法的好處在于無論獲取還是修改文章數據,都可以只對某一屬性進行操作,十分方便。而本章介紹的散列類型則更適合此場景,使用散列類型的存儲結構如圖3-7所示。
從圖3-7可以看出使用散列類型存儲文章數據比圖3-6所示的方法看起來更加直觀,也更容易維護(比如可以使用HGETALL命令獲得一個對象的所有字段,刪除一個對象時只需要刪除一個鍵),另外存儲同樣的數據散列類型往往比字符串類型更加節約空間,具體的細節會在4.6節中介紹。
使用過WordPress的讀者可能會知道發布文章時一般需要指定一個縮略名(slug)來構成該篇文章的網址的一部分,縮略名必須符合網址規范且最好可以與文章標題含義相似,如“This Is A Great Post!”的縮略名可以為“this-is-a-great-post”。每個文章的縮略名必須是唯一的,所以在發布文章時程序需要驗證用戶輸入的縮略名是否存在,同時也需要通過縮略名獲得文章的ID。
3-7{504}
圖3-7 使用一個散列類型鍵存儲一個對象
我們可以使用一個散列類型的鍵slug.to.id來存儲文章縮略名和ID之間的映射關系。其中字段用來記錄縮略名,字段值用來記錄縮略名對應的ID。這樣就可以使用HEXISTS命令來判斷縮略名是否存在,使用HGET命令來獲得縮略名對應的文章ID了。
現在發布文章可以修改成如下代碼:
$postID = INCR posts:count
# 判斷用戶輸入的slug是否可用,如果可用則記錄
$isSlugAvailable = HSETNX slug.to.id, $slug, $postID
if $isSlugAvailable is 0
# slug已經用過了,需要提示用戶更換slug,
# 這里為了演示方便直接退出。
exit
HMSET post:$postID, title, $title, content, $content, slug, $slug,...
這段代碼使用了HSETNX命令原子地實現了HEXISTS和HSET兩個命令以避免競態條件。當用戶訪問文章時,我們從網址中得到文章的縮略名,并查詢slug.to.id鍵來獲取文章ID:
$postID = HGET slug.to.id, $slug
if not $postID
print 文章不存在
exit
$post = HGETALL post:$postID
print 文章標題:$post.title
需要注意的是如果要修改文章的縮略名一定不能忘了修改slug.to.id鍵對應的字段。如要修改ID為42的文章的縮略名為newSlug變量的值:
# 判斷新的slug是否可用,如果可用則記錄
$isSlugAvailable = HSETNX slug.to.id, $newSlug, 42
if $isSlugAvailable is 0
exit
# 獲得舊的縮略名
$oldSlug = HGET post:42, slug
# 設置新的縮略名
HSET post:42, slug, $newSlug
# 刪除舊的縮略名
HDEL slug.to.id, $oldSlug
HKEYS key
HVALS key
有時僅僅需要獲取鍵中所有字段的名字而不需要字段值,那么可以使用HKEYS命令,就像這樣:
redis> HKEYS car
1) "name"
2) "model"
HVALS命令與HKEYS命令相對應,HVALS命令用來獲得鍵中所有字段值,例如:
redis> HVALS car
1) "BMW"
2) "C200"
HLEN key
例如:
redis> HLEN car
(integer) 2
正當小白躊躇滿志地寫著文章列表頁的代碼時,一個很重要的問題阻礙了他的開發,于是他請來了宋老師為他講解。
原來小白是使用如下流程獲得文章列表的:
對應的偽代碼如下:
# 每頁顯示10篇文章
$postsPerPage = 10
# 獲得最后發表的文章ID
$lastPostID = GET posts:count
# $currentPage存儲的是當前頁碼,第一頁時$currentPage的值為1,依此類推
$start = $lastPostID - ($currentPage - 1) * $postsPerPage
$end = max($lastPostID - $currentPage * $postsPerPage + 1, 1)
# 遍歷文章ID獲取數據
for $i = $start down to $end
# 獲取文章的標題和作者并打印出來
post = HMGET post:$i, title, author
print $post[0] # 文章標題
print $post[1] # 文章作者
可是這種方式要求用戶不能刪除文章以保證 ID 連續,否則小白就必須在程序中使用EXISTS命令判斷某個ID的文章是否存在,如果不存在則跳過。由于每刪除一篇文章都會影響后面的頁碼分布,為了保證每頁的文章列表都能正好顯示10篇文章,不論是第幾頁,都不得不從最大的文章ID開始遍歷來獲得當前頁面應該顯示哪些文章。
小白搖了搖頭,心想:“真是個災難!”然后看向宋老師,試探地問道:“我想到了KEYS命令,可不可以使用KEYS命令獲得所有以“post:”開頭的鍵,然后再根據鍵名分頁呢?”
宋老師回答道:“確實可行,不過KEYS命令需要遍歷數據庫中的所有鍵,出于性能考慮一般很少在生產環境中使用這個命令。至于你提到的問題,可以使用Redis的列表類型來解決。”
列表類型(list)可以存儲一個有序的字符串列表,常用的操作是向列表兩端添加元素,或者獲得列表的某一個片段。
列表類型內部是使用雙向鏈表(double linked list)實現的,所以向列表兩端添加元素的時間復雜度為O(1),獲取越接近兩端的元素速度就越快。這意味著即使是一個有幾千萬個元素的列表,獲取頭部或尾部的10條記錄也是極快的(和從只有20個元素的列表中獲取頭部或尾部的10條記錄的速度是一樣的)。
不過使用鏈表的代價是通過索引訪問元素比較慢,設想在iPad mini發售當天有1000個人在三里屯的蘋果店排隊等候購買,這時蘋果公司宣布為了感謝大家的排隊支持,決定獎勵排在第486位的顧客一部免費的iPad mini。為了找到這第486位顧客,工作人員不得不從隊首一個一個地數到第486個人。但同時,無論隊伍多長,新來的人想加入隊伍的話直接排到隊尾就好了,和隊伍里有多少人沒有任何關系。這種情景與列表類型的特性很相似。
這種特性使列表類型能非常快速地完成關系數據庫難以應付的場景:如社交網站的新鮮事,我們關心的只是最新的內容,使用列表類型存儲,即使新鮮事的總數達到幾千萬個,獲取其中最新的100條數據也是極快的。同樣因為在兩端插入記錄的時間復雜度是O(1),列表類型也適合用來記錄日志,可以保證加入新日志的速度不會受到已有日志數量的影響。
借助列表類型,Redis還可以作為隊列使用,4.4節會詳細介紹。
與散列類型鍵最多能容納的字段數量相同,一個列表類型鍵最多能容納232?1個元素。
LPUSH key value [value …]
RPUSH key value [value …]
LPUSH命令用來向列表左邊增加元素,返回值表示增加元素后列表的長度。
redis> LPUSH numbers 1
(integer) 1
這時numbers鍵中的數據如圖3-8所示。LPUSH命令還支持同時增加多個元素,例如:
redis> LPUSH numbers 2 3
(integer) 3
LPUSH會先向列表左邊加入"2",然后再加入"3",所以此時numbers鍵中的數據如圖3-9所示。
3-8{478}
{-:-}圖3-8 加入元素1后numbers鍵中的數據
3-9{454}
{-:-}圖3-9 加入元素2,3后numbers鍵中的數據
向列表右邊增加元素的話則使用RPUSH命令,其用法和LPUSH命令一樣:
redis> RPUSH numbers 0 ?1
(integer) 5
此時numbers鍵中的數據如圖3-10所示。
3-10{438}
圖3-10 使用RPUSH命令加入元素0,-1后numbers鍵中的數據
LPOP key
RPOP key
有進有出,LPOP命令可以從列表左邊彈出一個元素。LPOP命令執行兩步操作:第一步是將列表左邊的元素從列表中移除,第二步是返回被移除的元素值。例如,從numbers列表左邊彈出一個元素(也就是"3"):
redis> LPOP numbers
"3"
此時numbers鍵中的數據如圖3-11所示。
同樣,RPOP命令可以從列表右邊彈出一個元素:
redis> RPOP numbers
"-1"
此時numbers鍵中的數據如圖3-12所示。
結合上面提到的4個命令可以使用列表類型來模擬棧和隊列的操作:如果想把列表當做棧,則搭配使用LPUSH和LPOP或RPUSH和RPOP,如果想當成隊列,則搭配使用LPUSH和RPOP或RPUSH和LPOP。
3-11{438}
{-:-}圖3-11 從左側彈出元素后numbers鍵中的數據
3-12{438}
{-:-}圖3-12 從右側彈出元素后numbers鍵中的數據
LLEN key
當鍵不存在時LLEN會返回0:
redis> LLEN numbers
(integer) 3
LLEN命令的功能類似SQL語句SELECT COUNT(*) FROM table_name,但是LLEN的時間復雜度為O(1),使用時Redis會直接讀取現成的值,而不需要像部分關系數據庫(如使用InnoDB存儲引擎的MySQL表)那樣需要遍歷一遍數據表來統計條目數量。
LRANGE key start stop
LRANGE命令是列表類型最常用的命令之一,它能夠獲得列表中的某一片段。LRANGE命令將返回索引從start到stop之間的所有元素(包含兩端的元素)。與大多數人的直覺相同,Redis的列表起始索引為0:
redis> LRANGE numbers 0 2
1) "2"
2) "1"
3) "0"
LRANGE命令在取得列表片段的同時不會像LPOP一樣刪除該片段,另外LRANGE命令與很多語言中用來截取數組片段的方法slice有一點區別是LRANGE返回的值包含最右邊的元素,如在JavaScript中:
var numbers = [2, 1, 0];
console.log(numbers.slice(0, 2)); // 返回數組:[2, 1]
LRANGE命令也支持負索引,表示從右邊開始計算序數,如"?1"表示最右邊第一個元素,"-2"表示最右邊第二個元素,依次類推:
redis> LRANGE numbers -2 -1
1) "1"
2) "0"
顯然,LRANGE numbers 0 -1可以獲取列表中的所有元素。另外一些特殊情況如下。
1.如果start的索引位置比stop的索引位置靠后,則會返回空列表。
2.如果stop大于實際的索引范圍,則會返回到列表最右邊的元素:
redis> LRANGE numbers 1 999
1) "1"
2) "0"
LREM key count value
LREM命令會刪除列表中前count個值為value的元素,返回值是實際刪除的元素個數。根據count值的不同,LREM命令的執行方式會略有差異。
(1)當count > 0時LREM命令會從列表左邊開始刪除前count個值為value的元素。
(2)當count < 0時LREM命令會從列表右邊開始刪除前|count|個值為value的元素。
(3)當count = 0是LREM命令會刪除所有值為value的元素。例如:
redis> RPUSH numbers 2
(integer) 4
redis> LRANGE numbers 0 -1
1) "2"
2) "1"
3) "0"
4) "2"
# 從右邊開始刪除第一個值為"2"的元素
redis> LREM numbers -1 2
(integer) 1
redis> LRANGE numbers 0 -1
1) "2"
2) "1"
3) "0"
為了解決小白遇到的問題,我們使用列表類型鍵posts:list記錄文章ID列表。當發布新文章時使用LPUSH命令把新文章的ID加入這個列表中,另外刪除文章時也要記得把列表中的文章ID刪除,就像這樣:LREM posts:list 1要刪除的文章ID
有了文章ID列表,就可以使用LRANGE命令來實現文章的分頁顯示了。偽代碼如下:
$postsPerPage = 10
$start = ($currentPage - 1) * $postsPerPage
$end = $currentPage * $postsPerPage - 1
$postsID = LRANGE posts:list, $start, $end
# 獲得了此頁需要顯示的文章ID列表,我們通過循環的方式來讀取文章
for each $id in $postsID
$post = HGETALL post:$id
print 文章標題:$post.title
這樣顯示的文章列表是根據加入列表的順序倒序的(即最新發布的文章顯示在前面),如果想讓最舊的文章顯示在前面,可以使用LRANGE命令獲取需要的部分并在客戶端中將順序反轉顯示出來,具體的實現交由讀者來完成。
小白的問題至此就解決了,美中不足的一點是散列類型沒有類似字符串類型的MGET命令那樣可以通過一條命令同時獲得多個鍵的鍵值的版本,所以對于每個文章ID都需要請求一次數據庫,也就都會產生一次往返時延(round-trip delay time){![4.5節中還會詳細介紹這個概念。]},之后我們會介紹使用管道和腳本來優化這個問題。
另外使用列表類型鍵存儲文章ID列表有以下兩個問題。
(1)文章的發布時間不易修改:修改文章的發布時間不僅要修改post:文章ID中的time字段,還需要按照實際的發布時間重新排列posts:list中的元素順序,而這一操作相對比較繁瑣。
(2)當文章數量較多時訪問中間的頁面性能較差:前面已經介紹過,列表類型是通過鏈表實現的,所以當列表元素非常多時訪問中間的元素效率并不高。
但如果博客不提供修改文章時間的功能并且文章數量也不多時,使用列表類型也不失為一種好辦法。對于小白要做的博客系統來講,現階段的成果已經足夠實用且值得慶祝了。3.6節將介紹使用有序集合類型存儲文章ID列表的方法。
在博客中還可以使用列表類型鍵存儲文章的評論。由于小白的博客不允許訪客修改自己發表的評論,而且考慮到讀取評論時需要獲得評論的全部數據(評論者姓名,聯系方式,評論時間和評論內容),不像文章一樣有時只需要文章標題而不需要文章正文。所以適合將一條評論的各個元素序列化成字符串后作為列表類型鍵中的元素來存儲。
我們使用列表類型鍵post:文章ID:comments來存儲某個文章的所有評論。發布評論的偽代碼如下(以ID為42的文章為例):
# 將評論序列化成字符串
$serializedComment = serialize($author, $email, $time, $content)
LPUSH post:42:comments, $serializedComment
讀取評論時同樣使用LRANGE命令即可,具體的實現在此不再贅述。
LINDEX key index
LSET key index value
如果要將列表類型當作數組來用,LINDEX命令是必不可少的。LINDEX命令用來返回指定索引的元素,索引從0開始。如:
redis> LINDEX numbers 0
"2"
如果index是負數則表示從右邊開始計算的索引,最右邊元素的索引是?1。例如:
redis> LINDEX numbers -1
"0"
LSET是另一個通過索引操作列表的命令,它會將索引為index的元素賦值為value。例如:
redis> LSET numbers 1 7
OK
redis> LINDEX numbers 1
"7"
LTRIM key start end
LTRIM命令可以刪除指定索引范圍之外的所有元素,其指定列表范圍的方法和LRANGE命令相同。就像這樣:
redis> LRANGE numbers 0 -1
1) "1"
2) "2"
3) "7"
4) "3"
"0"
redis> LTRIM numbers 1 2
OK
redis> LRANGE numbers 0 1
1) "2"
2) "7"
LTRIM命令常和LPUSH命令一起使用來限制列表中元素的數量,比如記錄日志時我們希望只保留最近的100條日志,則每次加入新元素時調用一次LTRIM命令即可:
LPUSH logs $newLog
LTRIM logs 0 99
LINSERT key BEFORE|AFTER pivot value
LINSERT命令首先會在列表中從左到右查找值為pivot的元素,然后根據第二個參數是BEFORE還是AFTER來決定將value插入到該元素的前面還是后面。
LINSERT命令的返回值是插入后列表的元素個數。示例如下:
redis> LRANGE numbers 0 -1
1) "2"
2) "7"
3) "0"
redis> LINSERT numbers AFTER 7 3
(integer) 4
redis> LRANGE numbers 0 -1
1) "2"
2) "7"
3) "3"
4) "0"
redis> LINSERT numbers BEFORE 2 1
(integer) 5
redis> LRANGE numbers 0 -1
1) "1"
2) "2"
3) "7"
4) "3"
5) "0"
RPOPLPUSH source destination
RPOPLPUSH是個很有意思的命令,從名字就可以看出它的功能:先執行RPOP命令再執行LPUSH命令。RPOPLPUSH命令會先從source列表類型鍵的右邊彈出一個元素,然后將其加入到destination列表類型鍵的左邊,并返回這個元素的值,整個過程是原子的。其具體實現可以表示為偽代碼:
def rpoplpush ($source, $destination)
$value = RPOP $source
LPUSH $destination, $value
return $value
當把列表類型作為隊列使用時,RPOPLPUSH 命令可以很直觀地在多個隊列中傳遞數據。當source和destination相同時,RPOPLPUSH命令會不斷地將隊尾的元素移到隊首,借助這個特性我們可以實現一個網站監控系統:使用一個隊列存儲需要監控的網址,然后監控程序不斷地使用RPOPLPUSH命令循環取出一個網址來測試可用性。這里使用RPOPLPUSH命令的好處在于在程序執行過程中仍然可以不斷地向網址列表中加入新網址,而且整個系統容易擴展,允許多個客戶端同時處理隊列。
博客首頁,文章頁面,評論頁面……眼看著博客逐漸成型,小白的心情也是越來越好。時間已經到了深夜,小白卻還陶醉于編碼之中。不過一個他無法解決的問題最終還是讓他不得不提早睡覺去:小白不知道該怎么在Redis中存儲文章標簽(tag)。他想過使用散列類型或列表類型存儲,雖然都能實現,但是總覺得頗有不妥,再加上之前幾天領略了Redis的強大功能后,小白相信一定有一種合適的數據類型能滿足他的需求。于是小白給宋老師發了封詢問郵件后就睡覺去了。
轉天一早就收到了宋老師的回復:
你很善于思考嘛!你想的沒錯,Redis 有一種數據類型很適合存儲文章的標簽,它就是集合類型。
集合的概念高中的數學課就學習過。在集合中的每個元素都是不同的,且沒有順序。一個集合類型(set)鍵可以存儲至多232 ?1個(相信這個數字對大家來說已經很熟悉了)字符串。
集合類型和列表類型有相似之處,但很容易將它們區分開來,如表3-4所示。
表3-4 集合類型和列表類型對比
集 合 類 型列 表 類 型
存儲內容至多232 ?1個字符串至多232 ? 1個字符串
有序性否是
唯一性是否
集合類型的常用操作是向集合中加入或刪除元素、判斷某個元素是否存在等,由于集合類型在Redis內部是使用值為空的散列表(hash table)實現的,所以這些操作的時間復雜度都是O(1)。最方便的是多個集合類型鍵之間還可以進行并集、交集和差集運算,稍后就會看到靈活運用這一特性帶來的便利。
SADD key member [member …]
SREM key member [member …]
SADD命令用來向集合中增加一個或多個元素,如果鍵不存在則會自動創建。因為在一個集合中不能有相同的元素,所以如果要加入的元素已經存在于集合中就會忽略這個元素。本命令的返回值是成功加入的元素數量(忽略的元素不計算在內)。例如:
redis> SADD letters a
(integer) 1
redis> SADD letters a b c
(integer) 2
第二條SADD命令的返回值為2是因為元素“a”已經存在,所以實際上只加入了兩個元素。
SREM命令用來從集合中刪除一個或多個元素,并返回刪除成功的個數,例如:
redis> SREM letters c d
(integer) 1
由于元素“d”在集合中不存在,所以只刪除了一個元素,返回值為1。
SMEMBERS key
SMEMBERS命令會返回集合中的所有元素,例如:
redis> SMEMBERS letters
1) "b"
2) "a"
SISMEMBER key member
判斷一個元素是否在集合中是一個時間復雜度為O(1)的操作,無論集合中有多少個元素,SISMEMBER命令始終可以極快地返回結果。當值存在時SISMEMBER命令返回1,當值不存在或鍵不存在時返回0,例如:
redis> SISMEMBER letters a
(integer) 1
redis> SISMEMBER letters d
(integer) 0
SDIFF key [key …]
SINTER key [key …]
SUNION key [key …]
接下來要介紹的3個命令都是用來進行多個集合間運算的。
(1)SDIFF命令用來對多個集合執行差集運算。集合A與集合B的差集表示為A?_B_,代表所有屬于A且不屬于B的元素構成的集合(如圖3-13所示),即A?_B_ = {x |x∈_A_且x∈B}。例如:
{1, 2, 3} - {2, 3, 4} = {1}
{2, 3, 4} - {1, 2, 3} = {4}
SDIFF命令的使用方法如下:
redis> SADD setA 1 2 3
(integer) 3
redis> SADD setB 2 3 4
(integer) 3
redis> SDIFF setA setB
1) "1"
redis> SDIFF setB setA
1) "4"
SDIFF命令支持同時傳入多個鍵,例如:
redis> SADD setC 2 3
(integer) 2
redis> SDIFF setA setB setC
1) "1"
計算順序是先計算setA - setB,再計算結果與setC的差集。
(2)SINTER命令用來對多個集合執行交集運算。集合A與集合B的交集表示為A ∩ B,代表所有屬于A且屬于B的元素構成的集合(如圖3-14所示),即A ∩ B = {x | x ∈ A且_x_ ∈B}。例如:
{1, 2, 3} ∩ {2, 3, 4} = {2, 3}
SINTER命令的使用方法如下:
redis> SINTER setA setB
1) "2"
2) "3"
SINTER命令同樣支持同時傳入多個鍵,如:
redis> SINTER setA setB setC
1) "2"
2) "3"
(3)SUNION命令用來對多個集合執行并集運算。集合A與集合B的并集表示為A∪_B_,代表所有屬于A或屬于B的元素構成的集合(如圖3-15所示)即A∪_B_ = {x | x∈_A_或x ∈_B_}。例如:
{1, 2, 3} ∪ {2, 3, 4} = {1, 2, 3, 4}
3-14{200}
圖3-14 圖中斜線部分表示A ∩ B
3-15{200}
圖3-15 圖中斜線部分表示A ∪ B
SUNION命令的使用方法如下:
redis> SUNION setA setB
1) "1"
2) "2"
3) "3"
4) "4"
SUNION命令同樣支持同時傳入多個鍵,例如:
redis> SUNION setA setB setC
1) "1"
2) "2"
3) "3"
4) "4"
考慮到一個文章的所有標簽都是互不相同的,而且展示時對這些標簽的排列順序并沒有要求,我們可以使用集合類型鍵存儲文章標簽。
對每篇文章使用鍵名為post:文章ID:tags的鍵存儲該篇文章的標簽。具體操作如偽代碼:
# 給ID為42的文章增加標簽:
SADD post:42:tags, 閑言碎語, 技術文章, Java
# 刪除標簽:
SREM post:42:tags, 閑言碎語
# 顯示所有的標簽:
$tags = SMEMBERS post:42:tags
print $tags
使用集合類型鍵存儲標簽適合需要單獨增加或刪除標簽的場合。如在WordPress博客程序中無論是添加還是刪除標簽都是針對單個標簽的(如圖3-16所示),可以直觀地使用SADD和SREM命令完成操作。
另一方面,有些地方需要用戶直接設置所有標簽后一起上傳修改,圖3-17所示是某網站的個人資料編輯頁面,用戶編輯自己的愛好后提交,程序直接覆蓋原來的標簽數據,整個過程沒有針對單個標簽的操作,并未利用到集合類型的優勢,所以此時也可以直接使用字符串類型鍵存儲標簽數據。
3-16{384}
{-:-}圖3-16 在WordPress中設置文章標簽
3-17{534}
{-:-}圖3-17 在百度中設置個人愛好
之所以特意提到這個在實踐中的差別是想說明對于Redis存儲方式的選擇并沒有絕對的規則,比如3.4節介紹過使用列表類型存儲訪客評論,但是在一些特定的場合下散列類型甚至字符串類型可能更適合。
有時我們還需要列出某個標簽下的所有文章,甚至需要獲得同時屬于某幾個標簽的文章列表,這種需求在傳統關系數據庫中實現起來比較復雜,下面舉一個例子。
現有3張表,即posts、tags和posts_tags,分別存儲文章數據、標簽、文章與標簽的對應關系。結構分別如表3-5、表3-6、表3-7所示。
表3-5 posts表結構
字 段 名說 明
post_id文章ID
post_title文章標題
表3-6 tags表結構
字 段 名說 明
tag_id標簽ID
tag_name標簽名稱
表3-7 posts_tags表結構
字 段 名說 明
post_id對應的文章ID
tag_id對應的標簽ID
為了找到同時屬于“Java”、“MySQL”和“Redis”這3個標簽的文章,需要使用如下的SQL語句:
SELECT p.post_title
FROM posts_tags pt,
posts p,
tags t
WHERE pt.tag_id = t.tag_id
AND (t.tag_name IN ('Java', 'MySQL', 'Redis'))
AND p.post_id = pt.post_id
GROUP BY p.post_id HAVING COUNT(p.post_id)=3;
可以很明顯看到這樣的 SQL 語句不僅效率相對較低,而且不易閱讀和維護。而使用Redis可以很簡單直接地實現這一需求。
具體做法是為每個標簽使用一個名為tag:標簽名稱:posts的集合類型鍵存儲標有該標簽的文章ID列表。假設現在有3篇文章,ID分別為1、2、3,其中ID為1的文章標簽是“Java”,ID為2的文章標簽是“Java”、“MySQL”,ID為3的文章標簽是“Java”、 “MySQL”和“Redis”,則有關標簽部分的存儲結構如圖3-18所示{![集合類型鍵中元素是無序的,圖3-18中為了便于讀者閱讀將元素按照大小順序進行了排列。]}。
..18.tif{442}
圖3-18 和標簽有關部分的存儲結構
最簡單的,當需要獲取標記“MySQL”標簽的文章時只需要使用命令 SMEMBERS tag:MySQL:posts即可。如果要實現找到同時屬于Java、MySQL和Redis 3個標簽的文章,只需要將tag:Java:posts、tag:MySQL:posts和tag:Redis:posts這3個鍵取交集,借助SINTER命令即可輕松完成。
本文摘自《Redis入門指南》(第2版)
《Redis入門指南》(第2版)
本書是一本Redis的入門指導書籍,以通俗易懂的方式介紹了Redis基礎與實踐方面的知識,包括歷史與特性、在開發和生產環境中部署運行Redis、數據類型與命令、使用Redis實現隊列、事務、復制、管道、持久化、優化Redis存儲空間等內容,并采用任務驅動的方式介紹了PHP、Ruby、Python和Node.js這4種語言的Redis客戶端庫的使用方法。
本書的目標讀者不僅包括Redis的新手,還包括那些已經掌握Redis使用方法的人。對于新手而言,本書的內容由淺入深且緊貼實踐,旨在讓讀者真正能夠即學即用;對于已經了解Redis的讀者,通過本書的大量實例以及細節介紹,也能發現很多新的技巧。
1、parse()
在接收服務器數據時一般是字符串。
我們可以使用 JSON.parse() 方法將數據轉換為 JavaScript 對象
JSON.parse(result)
2、stringify()
在向服務器發送數據時一般是字符串。
我們可以使用 JSON.stringify() 方法將 JavaScript 對象轉換為字符串。
JSON.stringify(param)
3、serialize()
因為serialize()方法作用于jQuery對象,所以不光只有表單能使用它。返回字符串。
var $data = $(":checkbox,:radio").serialize();
or
var $data=$('.item-tag-box.active input').serialize();
$.get("get1.asp", $("#form1").serialize() , function (data, textStatus){
$("#resText").html(data); // 把返回的數據添加到頁面上
}
)
4、param()
它是serialize()方法的核心,用來對一個數組或對象按照key/value進行序列化
*請認真填寫需求信息,我們會在24小時內與您取得聯系。