Redis

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 数据库。  命令行下可以用 select 0、select 1 这样的指令切换数据库,最高为 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 已经存在,则覆盖之前的打分;  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 用来指定排序规则。 测试: db.SortedSetIncrement("Hotwords", "如鹏网", 1); db.SortedSetIncrement("Hotwords", "如鹏网", 1); db.SortedSetIncrement("Hotwords", "如鹏网", 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, double stop = 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 

{  

Console.WriteLine("操作中"); 

  Thread.Sleep(3000);  

Console.WriteLine("操作完成"); 

}

  finally

  { 

  db.LockRelease("mylock", token); 

}

}

else

Console.WriteLine("获得锁失败");

}  

21、抢红包案例

分析 redis 实现抢红包的案例,封上 100 元,随机发给 10 个人。

1) 随机红包的实现:先把 M 元钱平均分配给 N 个人(考虑最后一个除不尽的问题,分完 了算 N 个的和,如果多出来一些钱随机发给一个人);然后执行下面的操作 N 次:生成 两个随机的位置 i1、i2,i1 位置的金额为 m1,生成介于[0,m1/2)之间的随机金额 m2, 然后从 i1 位置减去这个 m2,再加到 i2 这个位置上。金额有可能是小数,精确到分,因 此为了避免精度损失,真实运算的时候都是按照分为单位,只有 int,没有 double。

2) 把这个红包数组以 List 的形式存到 Redis 中;

3) 用户抢红包就是从 List 中 Pop 取红包。
 
string s = "6.66";//红包总金额
int n = 10;//发给几个人
int m = (int)(Convert.ToDouble(s) * 100);//转换为分

int[] bags = new int[n];//n 个红包 int avg = m / n;

//算平均值

for(int i=0;i<n;i++)

bags[i] = avg;//先给每个红包平均分配

}
 
Random rand = new Random();
 int leftM = m - avg * n;//平均分配后可能会剩几分钱,随机发给一个人

bags[rand.Next(0,n)] += leftM;
 
for(int i=0;i<n;i++)

int i1 = rand.Next(0, n);//随机生成 i1、i2 两个位置 

int i2 = rand.Next(0, n); 

int delta = rand.Next(0, bags[i1]/2);//生成不高于第 i1 个红包目前余额一半的随机数 

bags[i1] -= delta;//从第 i1 个红包减掉 delta 钱 

bags[i2] += delta;//把钱加到第 i2 个红包上

}

if(bags.Sum()!=m)

throw new Exception("红包总金额不符");

}

原文地址:https://www.cnblogs.com/hudean/p/12698998.html