Redis .NET操作 1 安装 2 redis与Memcached 区别 3 redis命令行管理客户端 4 GUI管理客户端 5 .NET连接redis 6 命令 7 批量操作 8 分布式锁 9 抢红包案例

Redis是一个支持数据结构更多的键值对数据库。它的值不仅可以是字符串等基本数据类型,也可以是类对象,更可以是Set、List、计数器等高级的数据结构。
  Memcached也可以保存类似于Set、List这样的结构,但是如果说要向List中增加元素,Memcached则需要把List全部元素取出来,然后再把元素增加进去,然后再保存回去,不仅效率低,而且有并发访问问题。Redis内置的Set、List等可以直接支持增加、删除元素的操作,效率很高,操作是原子的。

Memcached数据存在内存中,memcached重启后数据就消失;
Redis会把数据持久化到硬盘中,Redis重启后数据还存在。

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的优点:

  1. 支持string、list、set、geo等复杂的数据结构。
  2. 高命中的数据运行时是在内存中,数据最终还是可以保存到磁盘中,这样服务器重启之后数据还在。
  3. 服务器是单线程的,来自所有客户端的所有命令都是串行执行的,因此不用担心并发修改(串行操作当然还是有并发问题)的问题,编程模型简单;
  4. 支持消息订阅/通知机制,可以用作消息队列;
  5. Key、Value最大长度允许512M;

2.1.2 redis的缺点:

  1. Redis是单线程的,因此单个Redis实例只能使用一个CPU核,不能充分发挥服务器的性能。可以在一台服务器上运行多个Redis实例,不同实例监听不同端口,再互相组成集群。
  2. 做缓存性能不如Memcached;

2.2 Memcached的优缺点

2.2.1 Memcached的优点:

  1. 多线程,可以充分利用CPU多核的性能;
  2. 做缓存性能最高;

2.2.2 Memcached的缺点:

  1. 只能保存键值对数据,键值对只能是字符串,如果有对象数据只能自己序列化成json
    字符串;
  2. 数据保存在内存中,重启后会丢失;
  3. 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
Redis .NET操作
1 安装
2 redis与Memcached 区别
3 redis命令行管理客户端
4 GUI管理客户端
5 .NET连接redis
6 命令
7 批量操作
8 分布式锁
9 抢红包案例

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(),尽量用异步方法。

注意看到访问的参数、返回值是RedisKeyRedisValue类型,进行了运算符重载,可以和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)

可以用 StringGetStringSet 来读写键值对,是基础操作
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>

Redis .NET操作
1 安装
2 redis与Memcached 区别
3 redis命令行管理客户端
4 GUI管理客户端
5 .NET连接redis
6 命令
7 批量操作
8 分布式锁
9 抢红包案例

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)。不传 startend 表示获取所有数据。指定之后则获取某个范围。

6.2.2 应用场景

  可以把Redis的list当成消息队列使用,比如向注册用户发送欢迎邮件的工作,可以在注册的流程中把要发送邮件的邮箱放到list中,另一个程序从list中pop获取邮件来发送。 生产者、消费者模式。把生产过程和消费过程隔离。
Redis .NET操作
1 安装
2 redis与Memcached 区别
3 redis命令行管理客户端
4 GUI管理客户端
5 .NET连接redis
6 命令
7 批量操作
8 分布式锁
9 抢红包案例

6.3 集合(Set)

  • Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
  • Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
  • 集合中最大的成员数为 232- 1 (4294967295, 每个集合可存储40多亿个成员)。

List与Set区别:
Redis .NET操作
1 安装
2 redis与Memcached 区别
3 redis命令行管理客户端
4 GUI管理客户端
5 .NET连接redis
6 命令
7 批量操作
8 分布式锁
9 抢红包案例

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应用场景

  1. 用户每搜一次一个关键词,就给这个关键词加一分;展示热搜的时候就把前N个获取出来就行了;
  2. 高积分用户排行榜;
  3. 热门商品;
  4. 给宝宝投票;

6.5 哈希(Hash)

  • Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。
  • Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)。

6.5.1 应用场景

存储文章数据
文章对象序列化后使用一个字符串类型键存储,可是这种方法无法提供对单个字段的原子读写操作,从而产生竞态条件。如两个客户端同事修改不同属性存储,后者覆盖前者。

使用多个字符串类型键存储一个对象,好处是只要修改一处属性,十分方便。
Redis .NET操作
1 安装
2 redis与Memcached 区别
3 redis命令行管理客户端
4 GUI管理客户端
5 .NET连接redis
6 命令
7 批量操作
8 分布式锁
9 抢红包案例
使用一个散列类型键存储一个对象更适合。散列更适合这个场景。
Redis .NET操作
1 安装
2 redis与Memcached 区别
3 redis命令行管理客户端
4 GUI管理客户端
5 .NET连接redis
6 命令
7 批量操作
8 分布式锁
9 抢红包案例

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 批量操作

如果一次性操作很多,会很慢,那么可以使用批量操作,两种方式:

  1. 几乎所有的操作都支持数组类型,这样就可以一次性操作多条数据:比如
GeoAdd(RedisKey key, GeoEntry[] values)
SortedSetAdd(RedisKey key, SortedSetEntry[] values) 
  1. 如果一次性的操作不是简单的同类型操作,那么就要使用批量模式:
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;
        }