Redis是一個支持數據結構更多的鍵值對數據庫。它的值不僅可以是字符串等基本數據類型,也可以是類對象,更可以是Set、List、計數器等高級的數據結構。
Memcached也可以保存類似於Set、List這樣的結構,但是如果說要向List中增加元素,Memcached則需要把List全部元素取出來,然后再把元素增加進去,然后再保存回去,不僅效率低,而且有並發訪問問題。Redis內置的Set、List等可以直接支持增加、刪除元素的操作,效率很高,操作是原子的。
Memcached數據存在內存中,memcached重啟后數據就消失;
Redis會把數據持久化到硬盤中,Redis重啟后數據還存在。
1 安裝
redis for windows >=2.8的版本支持直接安裝為windows服務(Redis-x64-3.2.100.msi才可以,zip不行)
https://github.com/MicrosoftArchive/redis
如果下載msi自動裝完服務,如果下載zip需要按照下面的方法安裝為服務:
http://www.runoob.com/redis/redis-install.html
2 redis與Memcached 區別
2.1 redis優缺點
2.1.1 redis的優點:
- 支持string、list、set、geo等復雜的數據結構。
- 高命中的數據運行時是在內存中,數據最終還是可以保存到磁盤中,這樣服務器重啟之后數據還在。
- 服務器是
單線程
的,來自所有客戶端的所有命令都是串行執行的,因此不用擔心並發修改(串行操作當然還是有並發問題)的問題,編程模型簡單; - 支持消息訂閱/通知機制,可以用作消息隊列;
- Key、Value最大長度允許512M;
2.1.2 redis的缺點:
- Redis是
單線程
的,因此單個Redis實例只能使用一個CPU核,不能充分發揮服務器的性能。可以在一台服務器上運行多個Redis實例,不同實例監聽不同端口,再互相組成集群。 - 做緩存性能不如Memcached;
2.2 Memcached的優缺點
2.2.1 Memcached的優點:
- 多線程,可以充分利用CPU多核的性能;
- 做緩存性能最高;
2.2.2 Memcached的缺點:
- 只能保存鍵值對數據,鍵值對只能是字符串,如果有對象數據只能自己序列化成json
字符串; - 數據保存在內存中,重啟后會丟失;
- Key最大長度255個字符,Value最長1M。
2.3 總結
Memcached只能當緩存服務器用,也是最合適的;Redis不僅可以做緩存服務器(性能沒有Memcached好),還可以存儲業務數據。
3 redis命令行管理客戶端
3.1 直接啟動redis安裝目錄下的redis-cli即可。
執行
set myKey abc
,就是設置鍵值對myKey=abc
執行get myKey
就是查找名字是myKey的值;
keys *
是查找所有的key
key *n*
是查找所有名字中含有n的key
3.2 數據沒有隔離性
和Redis一樣,Redis也是不同系統放到Redis中的數據都是不隔離的,因此設定Key的時候也要選擇好Key。
3.3 盡量選用默認的數據庫
Redis服務器默認建了16個數據庫,Redis的想法是讓大家把不同系統的數據放到不同的數據庫中。但是建議大家不要這樣用,因為Redis是單線程的,不同業務都放到同一個Redis實例的話效率就不高,建議放到不同的實例中。因此盡量只用默認的db0數據庫。
命令行下可以用select 0、select 1這樣的指令切換數據庫,最高為15。試試在不同數據庫下新建、查詢數據。
了解的常用的幾個命令就可以。所有對數據的操作都可以通過命令行進行,后面講的.net操作Redis的驅動其實就是對這些命令的封裝。
4 GUI管理客戶端
RedisDesktopManager (0.9.3以后需要訂閱)
https://github.com/uglide/RedisDesktopManager/releases
5 .NET連接redis
推薦組件:StackExchange.Redis
https://stackexchange.github.io/StackExchange.Redis/
其他作品:
NewLife.Redis基礎教程
https://www.cnblogs.com/nnhy/p/icache.html
using (ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost:6379"))
{
IDatabase db = redis.GetDatabase();
//默認是訪問db0數據庫,可以通過方法參數指定數字訪問不同的數據庫
db.StringSet("Name", "abc");
}
支持設置過期時間:
db.StringSet("name", "rupeng.com", TimeSpan.FromSeconds(10))
獲取數據:
string s = db.StringGet("Name")
//如果查不到則返回null
Redis里所有方法幾乎都支持異步,比如StringGetAsync()
、StringSetAsync()
,盡量用異步方法。
注意看到訪問的參數、返回值是RedisKey
、RedisValue
類型,進行了運算符重載,可以和string
、
byte[]
之間進行隱式轉換。
6 命令
6.1 鍵(Key)
因為Redis里所有數據類型都是用KeyValue保存,因此Key操作針對所有數據類型,
KeyDelete(RedisKey key)
:根據Key刪除;KeyExists(RedisKey key)
判斷Key是否存在,盡量不要用,因為會有並發問題;KeyExpire(RedisKey key, TimeSpan? expiry)
、KeyExpire(RedisKey key, DateTime? expiry)
設置過期時間;
6.2 字符串(String)
可以用 StringGet
、StringSet
來讀寫鍵值對,是基礎操作
StringAppend(RedisKey key, RedisValue value)
:向Key的Value中附加內容,不存在則新建;
6.2.1 場景:計數器
可以用作計數器:db.StringIncrement("count", 2.5)
; 給 count 這個計數器增加一個值,如果不存在
則從0開始加;db.StringDecrement("count",1)
計數器減值;獲取還是用StringGet()獲取字符串類型的
值。比如可以用這個來計算新聞點擊量、點贊量,效率非常高。
public class NewsController : Controller
{
private string NEWSPREFIX = "WX_NEWS_";
// GET: News
public async Task<ActionResult> Index(int id)
{
using (ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost:6379"))
{
IDatabase db = redis.GetDatabase();
//默認是訪問db0數據庫,可以通過方法參數指定數字訪問不同的數據庫
string clickCount = NEWSPREFIX + Request.UserHostAddress + "_ClickCount_" + id;
// Task<long> StringIncrementAsync:
// 返回值:The value of key after the increment. (遞增后的值)
long increment = await db.StringIncrementAsync(clickCount);
//RedisValue count = await db.StringGetAsync(clickCount);
//ViewBag.count = count;
ViewBag.count = increment;
}
return View();
}
}
index.cshtml
<h2>點擊量:@ViewBag.count</h2>
6.2 列表(List)
- Redis列表是簡單的字符串列表,按照插入順序排序。你可以添加一個元素到列表的頭部(左邊)或者尾部(右邊)
- 一個列表最多可以包含 232 - 1 個元素 (4294967295, 每個列表超過40億個元素)。
6.2.1 常用方法
1.從左側壓棧:
ListLeftPush(RedisKey key, RedisValue value)
;
2.從左側彈出:RedisValue ListLeftPop(RedisKey key)
;
3.從右側壓棧:ListRightPush(RedisKey key, RedisValue value )
;
4.從右側彈出:RedisValue ListRightPop(RedisKey key)
;
5.獲取Key為key的List中第index個元素的值:RedisValue ListGetByIndex(RedisKey key, long index) ;
6.獲取Key為key的List中元素個數:long ListLength(RedisKey key) ;
盡量不要用ListGetByIndex、ListLength因為會有並發問題。
如果是讀取而不pop,則使用ListRange:RedisValue[] ListRange(RedisKey key, long start = 0, long stop = -1)
。不傳start
、end
表示獲取所有數據。指定之后則獲取某個范圍。
6.2.2 應用場景
可以把Redis的list當成消息隊列使用,比如向注冊用戶發送歡迎郵件的工作,可以在注冊的流程中把要發送郵件的郵箱放到list中,另一個程序從list中pop獲取郵件來發送。 生產者、消費者模式。把生產過程和消費過程隔離。
6.3 集合(Set)
- Redis 的 Set 是 String 類型的無序集合。集合成員是唯一的,這就意味着集合中不能出現重復的數據。
- Redis 中集合是通過哈希表實現的,所以添加,刪除,查找的復雜度都是 O(1)。
- 集合中最大的成員數為 232- 1 (4294967295, 每個集合可存儲40多億個成員)。
List與Set區別:
6.3.1 常用方法
SetAdd(RedisKey key, RedisValue value)
向set中增加元素
bool SetContains(RedisKey key, RedisValue value)
判斷set中是否存在某個元素;
long SetLength(RedisKey key)
獲得set中元素的個數;
SetRemove(RedisKey key, RedisValue value)
從set中刪除元素;
RedisValue[] SetMembers(RedisKey key)
獲取a集合中的元素;
如果使用set保存封禁用id等,就不用做重復性判斷了。
6.4 有序集合(sorted set)
-
Redis 有序集合。
與Set不同的是,每個元素都會關聯一個double類型的分數。redis正是通過分數來為集合中的成員進行從小到大的排序。 -
有序集合的成員是唯一的,但分數(score)卻可以重復。
-
集合是通過哈希表實現的,所以添加,刪除,查找的復雜度都是O(1)。 集合中最大的成員數為 232- 1 (4294967295, 每個集合可存儲40多億個成員)。
6.4.1 常用方法
SortedSetAdd(RedisKey key, RedisValue member, double score)
在key這個sortedset中增加member,並且給這個member打分,如果member已經存在,則覆蓋之前的打分;
double SortedSetIncrement(RedisKey key, RedisValue member, double value)
給key中member這一項增加value分;
double SortedSetDecrement(RedisKey key, RedisValue member, double value)
給key中member這一項減value分;
SortedSetEntry[] SortedSetRangeByRankWithScores(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending)
根據排序返回 sortedset中的元素以及元素的打分,start、stop用來分頁查詢、order用來指定排序規則。
RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending)
根據打分排序返回值,可以根據序號查詢其中一部分;
RedisValue[] SortedSetRangeByScore(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1)
根據打分排序返回值,可以只返回start- stop 這個范圍的打分;
6.4.2 sortedset應用場景
- 用戶每搜一次一個關鍵詞,就給這個關鍵詞加一分;展示熱搜的時候就把前N個獲取出來就行了;
- 高積分用戶排行榜;
- 熱門商品;
- 給寶寶投票;
6.5 哈希(Hash)
- Redis hash 是一個string類型的field和value的映射表,hash特別適合用於存儲對象。
- Redis 中每個 hash 可以存儲 232 - 1 鍵值對(40多億)。
6.5.1 應用場景
存儲文章數據
文章對象序列化后使用一個字符串類型鍵存儲,可是這種方法無法提供對單個字段的原子讀寫操作,從而產生競態條件。如兩個客戶端同事修改不同屬性存儲,后者覆蓋前者。
使用多個字符串類型鍵存儲一個對象,好處是只要修改一處屬性,十分方便。
使用一個散列類型鍵存儲一個對象更適合。散列更適合這個場景。
6.6 GEO 地理位置
- Geo是Redis 3.2版本后新增的數據類型,用來保存興趣點(POI,point of interest)的坐標信息。
- 可以實現計算兩POI之間的距離、獲取一個點周邊指定距離的POI。
6.6.1 常用方法
1.下面添加興趣點數據,”1”、”2”是點的主鍵,點的名稱、地址、電話等存到其他表中。
db.GeoAdd("ShopsGeo", new GeoEntry(116.34039, 39.94218,"1"));
db.GeoAdd("ShopsGeo", new GeoEntry(116.340934, 39.942221, "2"));
db.GeoAdd("ShopsGeo", new GeoEntry(116.341082, 39.941025, "3"));
db.GeoAdd("ShopsGeo", new GeoEntry(116.340848, 39.937758, "4"));
db.GeoAdd("ShopsGeo", new GeoEntry(116.342982, 39.937325, "5"));
db.GeoAdd("ShopsGeo", new GeoEntry(116.340866, 39.936827, "6"));
2.刪除一個點
GeoRemove(RedisKey key, RedisValue member)
3.根據點的主鍵獲取坐標:
GeoPosition? pos = db.GeoPosition("ShopsGeo", "1")
4.查詢兩個POI之間的距離:
double? dist = db.GeoDistance("ShopsGeo", "1", "5", GeoUnit.Meters);//最后一個參數為距離單位
5.獲取一個POI周邊的POI:
GeoRadiusResult[] results = db.GeoRadius("ShopsGeo", "2", 200, GeoUnit.Meters);//獲取”2”這個周邊200米范圍內的POI
foreach(GeoRadiusResult result in results)
{
Console.WriteLine("Id="+result.Member+",位置"+result.Position+",距離"+result.Distance);
}
6.獲取一個坐標(這個坐標不一定是POI)周邊的POI:
GeoRadiusResult[] results = db.GeoRadius("ShopsGeo", 116.34092, 39.94223, 200, GeoUnit.Meters);// 獲取(116.34092, 39.94223)這個周邊200米范圍內的POI
foreach(GeoRadiusResult result in results)
{
Console.WriteLine("Id="+result.Member+",位置"+result.Position+",距離"+result.Distance);
}
Geo Hash原理:http://www.cnblogs.com/LBSer/p/3310455.html
7 批量操作
如果一次性操作很多,會很慢,那么可以使用批量操作,兩種方式:
- 幾乎所有的操作都支持數組類型,這樣就可以一次性操作多條數據:比如
GeoAdd(RedisKey key, GeoEntry[] values)
SortedSetAdd(RedisKey key, SortedSetEntry[] values)
- 如果一次性的操作不是簡單的同類型操作,那么就要使用批量模式:
IBatch batch = db.CreateBatch();
db.GeoAdd("ShopsGeo1", new GeoEntry(116.34039, 39.94218, "1"));
db.StringSet("abc", "123");
batch.Execute();
會把當前連接的CreateBatch()、Execute() 之間的操作一次性提交給服務器。
8 分布式鎖
多線程中的lock等的作用范圍是當前的程序范圍內的,如果想跨多台服務器的鎖(盡量避免這樣搞),就要使用分布式鎖
using (ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost:6379"))
{
IDatabase db = redis.GetDatabase();
RedisValue token = Environment.MachineName;
//實際項目秒殺此處可換成商品ID
//第三個參數為鎖超時時間,鎖占用最多10秒鍾,超過10秒鍾如果還沒有LockRelease,則也自動釋放鎖,避免了死鎖
if (db.LockTake("mylock", token, TimeSpan.FromSeconds(10)))
{
try
{
Console.WriteLine("操作開始~");
Thread.Sleep(30000);
Console.WriteLine("操作完成~");
}
finally
{
db.LockRelease("mylock", token);
}
}
else
{
Console.WriteLine("獲得鎖失敗");
}
Console.ReadKey();
}
9 搶紅包案例
- 把這個紅包數組以List的形式存到Redis中;
- 用戶搶紅包就是從List中Pop取紅包。
發出一個固定金額的紅包,由若干個人來搶,需要滿足哪些規則?
1.所有人搶到金額之和等於紅包金額,不能超過,也不能少於。
2.每個人至少搶到一分錢。
3.要保證所有人搶到金額的幾率相等。
9.1 二倍均值法
參考:程序員小灰——漫畫:如何實現搶紅包算法?
剩余紅包金額為M,剩余人數為N,那么有如下公式:
每次搶到的金額 = 隨機區間 (0, M / N X 2)
這個公式,保證了每次隨機金額的平均值是相等的,不會因為搶紅包的先后順序而造成不公平。
舉個栗子:
假設有10個人,紅包總額100元。100/10X2 = 20, 所以第一個人的隨機范圍是(0,20 ),平均可以搶到10元。
假設第一個人隨機到10元,那么剩余金額是100-10 = 90 元。90/9X2 = 20, 所以第二個人的隨機范圍同樣是(0,20 ),平均可以搶到10元。
假設第二個人隨機到10元,那么剩余金額是90-10 = 80 元。80/8X2 = 20, 所以第三個人的隨機范圍同樣是(0,20 ),平均可以搶到10元。
以此類推,每一次隨機范圍的均值是相等的。
static void Main(string[] args)
{
//例子:50元分配10個人
List<int> amountList = divideRedPackage(5000, 10);
foreach (double amount in amountList)
{
double item = (amount / 100);
Console.WriteLine($"搶到金額:{item}");
}
}
//發紅包算法,金額參數以分為單位
public static List<int> divideRedPackage(int totalAmount, int totalPeopleNum)
{
List<int> amountList = new List<int>();
int restAmount = totalAmount;
int restPeopleNum = totalPeopleNum;
Random random = new Random();
for (int i = 0; i < totalPeopleNum - 1; i++)
{
//隨機范圍:[1,剩余人均金額的兩倍),左閉右開
int amount = random.Next(restAmount / restPeopleNum * 2 - 1) + 1;
restAmount -= amount;
restPeopleNum--;
amountList.Add(amount);
}
amountList.Add(restAmount);
return amountList;
}