1、Redis 简介
Redis 是一个支持数据结构更多的键值对数据库。它的值不仅可以是字符串等基本数据
类型,也可以是类对象,更可以是 Set、List、计数器等高级的数据结构。
Memcached 也可以保存类似于 Set、List 这样的结构,但是如果说要向 List 中增加元素,
Memcached 则需要把 List 全部元素取出来,然后再把元素增加进去,然后再保存回去,不
仅效率低,而且有并发访问问题。
Redis 内置的 Set、List 等可以直接支持增加、删除元素的操作,效率很高,操作是原子的。
Memcached 数据存在内存中,memcached 重启后数据就消失;而 Redis 会把数据持久
化到硬盘中,Redis 重启后数据还存在。
2、Redis 的安装
redis for windows >=2.8 的版本支持直接安装为 windows 服务
https://github.com/MicrosoftArchive/redis
如果下载 msi 自动装完服务,如果下载 zip 需要按照下面的方法安装为服务:
https://raw.githubusercontent.com/MSOpenTech/redis/3.0/Windows%20Service%20Documenta
tion.md
3、redis 的优点:
-
1) 支持 string、list、set、geo 等复杂的数据结构。
-
2) 高命中的数据运行时是在内存中,数据最终还是可以保存到磁盘中,这样服务器重启之后数据还在。
-
3) 服务器是单线程的,来自所有客户端的所有命令都是串行执行的,因此不用担心并发修改(串行操作当然还是有并发问题)的问题,编程模型简单;
-
4) 支持消息订阅/通知机制,可以用作消息队列;
-
5) Key、Value 最大长度允许 512M;
4、redis 的缺点:
-
1) Redis 是单线程的,因此单个 Redis 实例只能使用一个 CPU 核,不能充分发挥服务器的性能。可以在一台服务器上运行多个 Redis 实例,不同实例监听不同端口,再互相组成集群。
-
2) 做缓存性能不如 Memcached;
5、Memcached 的优点:
1) 多线程,可以充分利用 CPU 多核的性能;
2) 做缓存性能最高;
6、Memcached 的缺点:
-
1) 只能保存键值对数据,键值对只能是字符串,如果有对象数据只能自己序列化成 json字符串;
-
2) 数据保存在内存中,重启后会丢失;
-
3) Key 最大长度 255 个字符,Value 最长 1M。
7、总结
Memcached 只能当缓存服务器用,也是最合适的;Redis 不仅可以做缓存服务器(性能没有 Memcached 好),还可以存储业务数据。
8、redis 命令行管理客户端:
1)直接启动 redis 安装目录下的 redis-cli 即可。不用管恶心的自动提示。 执行 set name yzk,就是设置键值对 name=yzk 执行 get name 就是查找名字是 name 的值; keys *是查找所有的 key key *n*是查找所有名字中含有 n 的 key
2) 和 Redis 一样,Redis 也是不同系统放到 Redis 中的数据都是不隔离的,因此设定 Key 的
时候也要选择好 Key。
3) Redis 服务器默认建了 16 个数据库,Redis 的想法是让大家把不同系统的数据放到不同
的数据库中。但是建议大家不要这样用,因为 Redis 是单线程的,不同业务都放到同一个 Redis
实例的话效率就不高,建议放到不同的实例中。
因此尽量只用默认的 db0数据库命令行下可以用 select0、select1 这样的指令切换数据库,最高为15。试试在不同数据 库下新建、查询数据。
4) 了解的常用的几个命令就可以。所有对数据的操作都可以通过命令行进行,后面讲 的.net 操作 Redis 的驱动其实就是对这些命令的封装。
9、redis GUI 管理客户端
GUI 客户端非常多,个人推荐使用 RedisDesktopManager安装后点击【Connect to Redis Server】连接服务器。展开节点可以看到所有的 Key,双击 Key 可以查看 Key 的值。在根节点上点右键,选择 【Console】,这样就可以输入命令。
10、.net 操作 Redis
用 StackExchange.Redis ,而不是 ServiceStack.Redis,因为 StackExchange.Redis 依赖组件 少,而且操作更接近原生的 redis 操作,ServiceStack 封装的太厉害,而且有过收费的“前科”。
Install-Package StackExchange.Redis
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[]之间进行隐式转换。
11、Key 操作
Key 操作:因为 Redis 里所有数据类型都是用 KeyValue 保存,因此 Key 操作针对所有数据类型,
KeyDelete(RedisKey key):根据 Key 删除;KeyExists(RedisKey key)
判断 Key 是否存在,尽量不要用,
因为会有并发问题;KeyExpire(RedisKey key, TimeSpan? expiry)、KeyExpire(RedisKey key, DateTime?
expiry)设置过期时间;
12、数据类型
Redis 支持的数据结构:string、list、set、sortedset、hash、geo(redis 3.2 以上版本)。对应 的 Redis 客户端里的方法都是 StringXXX、HashXXX、GeoXXX 等方法。
不同数据类型的操作方
法不能混用,比如不能用 ListXXX 写入的值用 StringXXX 去读取或者写 入等操作。
13、String 类型
可以用 StringGet、StringSet 来读写键值对,是基础操作StringAppend(RedisKey key, RedisValue value):向 Key 的 Value 中附加内容,不存在则新建; 可以用作计数器:db.StringIncrement("count", 2.5);
给 count 这个计数器增加一个值,如果不存在则从 0 开始加;db.StringDecrement("count",1)计数器减值;获取还是用 StringGet()获取字符串类型的 值。比如可以用这个来计算新闻点击量、点赞量,效率非常高。
private static string XinWen_Prefix = "WWW_XinWen_"; public async Task<ActionResult> Index(int id) { using (ConnectionMultiplexer redis = await ConnectionMultiplexer.ConnectAsync("localhost:6379")) { IDatabase db = redis.GetDatabase();//默认是访问 db0 数据库,可以通过方法参数指定数字访 问不同的数据库 //以 ip 地址和文章 id 为 key string hasClickKey = XinWen_Prefix + Request.UserHostAddress + "_" + id; //如果之前这个 ip 给这个文章贡献过点击量,则不重复计算点击量 if(await db.KeyExistsAsync(hasClickKey)==false) { await db.StringIncrementAsync(XinWen_Prefix + "XWClickCount" + id, 1); //记录一下这个 ip 给这个文章贡献过点击量,有效期一天 db.StringSet(hasClickKey, "a", TimeSpan.FromDays(1)); } RedisValue clickCount = await db.StringGetAsync(XinWen_Prefix + "XWClickCount" + id); XinWenModel model = new XinWenModel(); model.ClickCount = Convert.ToInt32(clickCount); return View(model); } return View(); }
14、list 类型
Redis 中用 List 保存字符串集合。 比如可以把聊天记录保存到 List 中;商品的物流信息记录。也 可以当成双向队列或者双向栈用,list 长度是无限。
ListLeftPush(RedisKey key, RedisValue value)从左侧压栈;RedisValue ListLeftPop(RedisKey key) 从左侧弹出;
ListRightPush(RedisKey key, RedisValue value ) 从右侧压栈;RedisValue ListRightPop(RedisKey key) 从右侧弹出;
RedisValue ListGetByIndex(RedisKey key, long index)获取 Key 为 key 的 List 中第 index 个元素的值; long ListLength(RedisKey key) 获取 Key 为 key 的 List 中元素个数;尽量不要用 ListGetByIndex、 ListLength 因为会有并发问题;。
如果是读取而不 Pop,则使用 ListRange:RedisValue[] ListRange(RedisKey key, long start = 0, long stop = -1)。不传 start、end 表示获取所有数据。指定之后则获取某个范围。
可以把 Redis 的 list 当成消息队列使用,比如向注册用户发送欢迎邮件的工作,可以在注册的流 程中把要发送邮件的邮箱放到 list 中,另一个程序从 list 中 pop 获取邮件来发送。
生产者、消费者模式。把生产过程和消费过程隔离。
15、set 类型
如大家所知,set 是一种元素不重复的集合。
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)获取集合中的元素;
如果使用 set 保存封禁用 id 等,就不用做重复性判断了。
注意 set 不是按照插入顺序遍历的,而是按照自己的一个存储方式来遍历,因为没有保存插入的 顺序。
16、sortedset
如果对于数据遍历顺序有要求,可以使用 sortedset,他会按照打分来进行遍历。
SortedSetAdd(RedisKey key, RedisValue member, double score) 在 key 这个 sortedset 中增加member,并且给这个 member 打分,如果 member 已经存在,则覆盖之前的打分; doubleSortedSetIncrement(RedisKeykey,RedisValuemember,doublevalue) 给key中member这一项增加 value 分;
double SortedSetDecrement(RedisKey key, RedisValue member, double value):给 key 中 member 这一项减 value 分;
SortedSetEntry[] SortedSetRangeByRankWithScores(RedisKey key, long start = 0, long stop = -1,Orderorder=Order.Ascending) 根据排序返回sortedset中的元素以及元素的打分,start、stop用来分页 查询、order 用来指定排序规则。
测试:
db.SortedSetIncrement("Hotwords", "test", 1); db.SortedSetIncrement("Hotwords", "test", 1); db.SortedSetIncrement("Hotwords", "test", 1); db.SortedSetIncrement("Hotwords", "杨中科", 1); db.SortedSetIncrement("Hotwords", "侯宝林", 1); db.SortedSetIncrement("Hotwords", "侯宝林", 1); SortedSetEntry[] items = db.SortedSetRangeByRankWithScores("Hotwords"); foreach(var item in items) { Console.WriteLine(item.Element+"="+item.Score); }
RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop = -1, Order order =Order.Ascending) 根据打分排序返回值,可以根据序号查询其中一部分;
RedisValue[] SortedSetRangeByScore(RedisKey key, double start = double.NegativeInfinity, doublestop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1)
根据打分排序返回值,可以只返回 start- stop 这个范围的打分;
sortedset 应用场景:
1) 用户每搜一次一个关键词,就给这个关键词加一分;展示热搜的时候就把前 N 个获取出来就行了;
2) 高积分用户排行榜;
3) 热门商品;
4) 给宝宝投票;
17、Hash
相当于 value 又是一个“键值对集合”或者值是另外一个 Dictionary。 没想到有什么应用场景。
18、Geo 类型
Geo 是 Redis 3.2 版本后新增的数据类型,用来保存兴趣点(POI,point of interest)的坐标信息。
可以实现计算两 POI 之间的距离、获取一个点周边指定距离的 POI。 下面添加兴趣点数据,”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"));
GeoRemove(RedisKey key, RedisValue member)删除一个点
查询两个 POI 之间的举例:double? dist = db.GeoDistance("ShopsGeo", "1", "5", GeoUnit.Meters);// 最后一个参数为距离单位根据点的主键获取坐标:GeoPosition? pos = db.GeoPosition("ShopsGeo", "1")
获取一个 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); }
获取一个坐标(这个坐标不一定是 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
19、Redis 的批量操作
如果一次性操作很多,会很慢,那么可以使用批量操作,两种方式: 1)几乎所有的操作都支持数组类型,这样就可以一次性操作多条数据:比如
GeoAdd(RedisKey key, GeoEntry[] values)、SortedSetAdd(RedisKey key, SortedSetEntry[] values) 2) 如果一次性的操作不是简单的同类型操作,那么就要使用批量模式:
IBatch batch = db.CreateBatch();
db.GeoAdd("ShopsGeo1", new GeoEntry(116.34039, 39.94218, "1"));
db.StringSet("abc", "123"); batch.Execute();
会把当前连接的 CreateBatch()、Execute()之间的操作一次性提交给服务器。
20、redis 分布式锁
多线程中的 lock 等的作用范围是当前的程序范围内的,如果想跨多台服务器的锁(尽量避免这样搞),就要使用分布式锁。
RedisValue token = Environment.MachineName; //实际项目秒杀此处可换成商品 ID if (db.LockTake("mylock", token, TimeSpan.FromSeconds(10)))//第三个参数为锁超时时间,锁占 用最多 10 秒钟,超过 10 秒钟如果还没有 LockRelease,则也自动释放锁,避免了死锁 { try { } finally { db.LockRelease("mylock", token); } } else { Console.WriteLine("获得锁失败"); }