【Redis】Redis由浅入深代码示例
一、下载
https://github.com/MicrosoftArchive/redis/releases
Redis支持32位和64位。这个需要根据你系统平台的实际情况选择,这里下载 Redis-x64-xxx.zip压缩包到 D 盘redis文件夹下。
解压:
二、Redis临时服务
1.打开cmd,进入到刚才解压到的目录,启动临时服务:redis-server.exe redis.windows.conf
(备注:通过这个命令,会创建Redis临时服务,不会在window服务列表出现Redis服务名称和状态,此窗口关闭,服务会自动关闭。)
2.打开另一个cmd窗口,客户端调用:redis-cli.exe -h 127.0.0.1 -p 6379
(默认6379端口:6379在是手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字。MERZ长期以来被antirez及其朋友当作愚蠢的代名词。Redis作者antirez同学在twitter上说将在下一篇博文中向大家解释为什么他选择6379作为默认端口号......😯)
三、Redis自定义Windows服务安装
1.进入Redis安装包目录,安装服务
redis-server.exe --service-install redis.windows.conf --service-name redisserver1 --loglevel verbose
win+r -> services.msc,可以看到服务安装成功
安装服务:redis-server.exe --service-install redis.windows.conf --service-name redisserver1 --loglevel verbose
启动服务:redis-server.exe --service-start --service-name redisserver1
停止服务:redis-server.exe --service-stop --service-name redisserver1
卸载服务:redis-server.exe --service-uninstall --service-name redisserver1
四、主从服务
1.将d盘下新建一个文件夹叫redis2,把redis文件夹的东西拷贝到redis2文件夹下,将redis-windows.conf配置文件中的ip 和端口号改一下,然后按照上面的步骤安装一个服务叫redisserver2
2.使用redis桌面管理器(下载地址:https://redisdesktop.com/),链接两个redis库
3.设置密码把redis.windows.conf文件中 #requirepass foobared 的#号去掉改为自己的密码即可(requirepass前面不能有空格,不然服务启动报错)
4.设置好保存后,若要使设置起作用,需要重启redis服务
5.端口号和ip同理
6.重启后需要输入密码
7.slaveof 127.0.0.1 6379 设置主从,6379是主库,6380是从库。(设置同步时,会将主库所有数据一起同步过来。)
五、哨兵模式
1.在主服务器redis文件夹下新建文件:sentinel.conf
输入:sentinel monitor host6379 127.0.0.1 6379 1
2.执行 redis-server.exe sentinel.conf --sentinel
六、StackExchange客户端
using StackExchange.Redis; using System; namespace ConsoleApp1 { class Program { #region ConnectionMultiplexer是StackExchange.Redis的核心,它被整个应用程序共享和重用,应该设置为单例 // redis config // 哨兵26379 主从服务器6379,6380 private static readonly ConfigurationOptions ConfigurationOptions = ConfigurationOptions.Parse("127.0.0.1:26379,127.0.0.1:6379,127.0.0.1:6380,password='',connectTimeout=60"); //the lock for singleton private static readonly object Locker = new object(); //singleton private static ConnectionMultiplexer _redisConn; //singleton public static ConnectionMultiplexer GetRedisConn() { if (_redisConn == null) { lock (Locker) { if (_redisConn == null || !_redisConn.IsConnected) { _redisConn = ConnectionMultiplexer.Connect(ConfigurationOptions); } } } return _redisConn; } #endregion static void Main(string[] args) { _redisConn = GetRedisConn(); IDatabase db = _redisConn.GetDatabase(); #region string var strKey = "hello"; var strValue = "world"; db.StringSet(strKey, strValue); #endregion #region hash 使用场景:微博个人信息 string hashKey = "myhash"; //hset db.HashSet(hashKey, "f1", "v1"); db.HashSet(hashKey, "f2", "v2"); HashEntry[] values1 = db.HashGetAll(hashKey); //hgetall Console.Write("hgetall " + hashKey + ", result is"); foreach (HashEntry hashEntry in values1) { Console.Write(" " + hashEntry.Name + " " + hashEntry.Value); } #endregion #region list 使用场景:微博粉丝 //list key string listKey = "myList"; //rpush db.ListRightPush(listKey, "a"); db.ListRightPush(listKey, "b"); db.ListRightPush(listKey, "c"); //lrange RedisValue[] values = db.ListRange(listKey, 0, -1); Console.Write("lrange " + listKey + " 0 -1, result is "); for (int i = 0; i < values.Length; i++) { Console.Write(values[i] + " "); } Console.WriteLine(); #endregion #region set 使用场景:队列 //set key string setKey = "mySet"; //sadd db.SetAdd(setKey, "a"); db.SetAdd(setKey, "b"); db.SetAdd(setKey, "c"); //sismember bool isContains = db.SetContains(setKey, "a"); Console.WriteLine("set " + setKey + " contains a is " + isContains); #endregion #region sortedset 使用场景:队列 string sortedSetKey = "myZset"; //sadd db.SortedSetAdd(sortedSetKey, "xiaoming", 85); db.SortedSetAdd(sortedSetKey, "xiaohong", 100); db.SortedSetAdd(sortedSetKey, "xiaofei", 62); db.SortedSetAdd(sortedSetKey, "xiaotang", 73); //zrevrangebyscore RedisValue[] names = db.SortedSetRangeByRank(sortedSetKey, 0, 2, Order.Ascending); Console.Write("zrevrangebyscore " + sortedSetKey + " 0 2, result is "); for (int i = 0; i < names.Length; i++) { Console.Write(names[i] + " "); } Console.WriteLine(); #endregion Console.ReadLine(); } } }
如果主服务器挂了,从服务器不能立刻变为主库,写数据会失败,因为哨兵切换从库为主库时默认30秒(sentinel.conf配置文件中配置)
哨兵临时服务窗口不能关闭(上一步的操作)
如果关闭需要安装为windows服务守护进程:redis-server --service-install sentinel.conf --sentinel --service-name RedisSentinel --port 26379
sentinel配置说明:
# Example sentinel.conf # 哨兵sentinel实例运行的端口 默认26379 port 26379 # 哨兵sentinel的工作目录 dir /tmp # 哨兵sentinel监控的redis主节点的 ip port # master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。 # quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了 # sentinel monitor <master-name> <ip> <redis-port> <quorum> sentinel monitor mymaster 127.0.0.1 6379 2 # 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码 # 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码 # sentinel auth-pass <master-name> <password> sentinel auth-pass mymaster MySUPER--secret-0123passw0rd # 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒 # sentinel down-after-milliseconds <master-name> <milliseconds> sentinel down-after-milliseconds mymaster 30000 # 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步, 这个数字越小,完成failover所需的时间就越长, 但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。 可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。 # sentinel parallel-syncs <master-name> <numslaves> sentinel parallel-syncs mymaster 1 # 故障转移的超时时间 failover-timeout 可以用在以下这些方面: #1. 同一个sentinel对同一个master两次failover之间的间隔时间。 #2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。 #3.当想要取消一个正在进行的failover所需要的时间。 #4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了 # 默认三分钟 # sentinel failover-timeout <master-name> <milliseconds> sentinel failover-timeout mymaster 180000 # SCRIPTS EXECUTION #配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。 #对于脚本的运行结果有以下规则: #若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10 #若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。 #如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。 #一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。 #通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本, 这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数, 一个是事件的类型, 一个是事件的描述。 如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。 #通知脚本 # sentinel notification-script <master-name> <script-path> sentinel notification-script mymaster /var/redis/notify.sh # 客户端重新配置主节点参数脚本 # 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。 # 以下参数将会在调用脚本时传给脚本: # <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port> # 目前<state>总是“failover”, # <role>是“leader”或者“observer”中的一个。 # 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的 # 这个脚本应该是通用的,能被多次调用,不是针对性的。 # sentinel client-reconfig-script <master-name> <script-path> sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
redis.windows.conf配置文件说明:
daemonize yes #是否以后台进程运行 pidfile /var/run/redis/redis-server.pid #pid文件位置 port 6379#监听端口 bind 127.0.0.1 #绑定地址,如外网需要连接,设置0.0.0.0 timeout 300 #连接超时时间,单位秒 loglevel notice #日志级别,分别有: # debug :适用于开发和测试 # verbose :更详细信息 # notice :适用于生产环境 # warning :只记录警告或错误信息 logfile /var/log/redis/redis-server.log #日志文件位置 syslog-enabled no #是否将日志输出到系统日志 databases 16#设置数据库数量,默认数据库为0 ############### 快照方式 ############### save 900 1 #在900s(15m)之后,至少有1个key发生变化,则快照 save 300 10 #在300s(5m)之后,至少有10个key发生变化,则快照 save 60 10000 #在60s(1m)之后,至少有1000个key发生变化,则快照 rdbcompression yes #dump时是否压缩数据 dir /var/lib/redis #数据库(dump.rdb)文件存放目录 ############### 主从复制 ############### slaveof <masterip> <masterport> #主从复制使用,用于本机redis作为slave去连接主redis masterauth <master-password> #当master设置密码认证,slave用此选项指定master认证密码 slave-serve-stale-data yes #当slave与master之间的连接断开或slave正在与master进行数据同步时,如果有slave请求,当设置为yes时,slave仍然响应请求,此时可能有问题,如果设置no时,slave会返回"SYNC with master in progress"错误信息。但INFO和SLAVEOF命令除外。 ############### 安全 ############### requirepass foobared #配置redis连接认证密码 ############### 限制 ############### maxclients 128#设置最大连接数,0为不限制 maxmemory <bytes>#内存清理策略,如果达到此值,将采取以下动作: # volatile-lru :默认策略,只对设置过期时间的key进行LRU算法删除 # allkeys-lru :删除不经常使用的key # volatile-random :随机删除即将过期的key # allkeys-random :随机删除一个key # volatile-ttl :删除即将过期的key # noeviction :不过期,写操作返回报错 maxmemory-policy volatile-lru#如果达到maxmemory值,采用此策略 maxmemory-samples 3 #默认随机选择3个key,从中淘汰最不经常用的 ############### 附加模式 ############### appendonly no #AOF持久化,是否记录更新操作日志,默认redis是异步(快照)把数据写入本地磁盘 appendfilename appendonly.aof #指定更新日志文件名 # AOF持久化三种同步策略: # appendfsync always #每次有数据发生变化时都会写入appendonly.aof # appendfsync everysec #默认方式,每秒同步一次到appendonly.aof # appendfsync no #不同步,数据不会持久化 no-appendfsync-on-rewrite no #当AOF日志文件即将增长到指定百分比时,redis通过调用BGREWRITEAOF是否自动重写AOF日志文件。 ############### 虚拟内存 ############### vm-enabled no #是否启用虚拟内存机制,虚拟内存机将数据分页存放,把很少访问的页放到swap上,内存占用多,最好关闭虚拟内存 vm-swap-file /var/lib/redis/redis.swap #虚拟内存文件位置 vm-max-memory 0 #redis使用的最大内存上限,保护redis不会因过多使用物理内存影响性能 vm-page-size 32 #每个页面的大小为32字节 vm-pages 134217728 #设置swap文件中页面数量 vm-max-threads 4 #访问swap文件的线程数 ############### 高级配置 ############### hash-max-zipmap-entries 512 #哈希表中元素(条目)总个数不超过设定数量时,采用线性紧凑格式存储来节省空间 hash-max-zipmap-value 64 #哈希表中每个value的长度不超过多少字节时,采用线性紧凑格式存储来节省空间 list-max-ziplist-entries 512 #list数据类型多少节点以下会采用去指针的紧凑存储格式 list-max-ziplist-value 64 #list数据类型节点值大小小于多少字节会采用紧凑存储格式 set-max-intset-entries 512 #set数据类型内部数据如果全部是数值型,且包含多少节点以下会采用紧凑格式存储 activerehashing yes #是否激活重置哈希
一主二从三哨兵
redis主从:是备份关系, 我们操作主库,数据也会同步到从库。 如果主库机器坏了,从库可以上。就好比你 D盘的片丢了,但是你移动硬盘里边备份有。
redis哨兵:哨兵保证的是HA,保证特殊情况故障自动切换,哨兵盯着你的“redis主从集群”,如果主库死了,它会告诉你新的老大是谁。
redis集群:集群保证的是高并发,因为多了一些兄弟帮忙一起扛。同时集群会导致数据的分散,整个redis集群会分成一堆数据槽,即不同的key会放到不不同的槽中。
主从保证了数据备份,哨兵保证了HA 即故障时切换,集群保证了高并发性。
七、集群搭建(转)
原文地址:https://blog.csdn.net/hao495430759/article/details/80540407
1.首先我们构建集群节点目录:
(集群正常运作至少需要三个主节点,不过在刚开始试用集群功能时, 强烈建议使用六个节点: 其中三个为主节点, 而其余三个则是各个主节点的从节点。主节点崩溃,从节点的Redis就会提升为主节点,代替原来的主节点工作,崩溃的主Redis回复工作后,会成为从节点)
拷贝开始下载的redis解压后的目录,并修改文件名(比如按集群下redis端口命名)如下:
6380,6381,6382,6383,6384,6385对应的就是后面个节点下启动redis的端口。
在节点目录下新建文件,输入(举例在6380文件夹下新建文件)
title redis-6380;
redis-server.exe redis.windows.conf
然后保存为start.bat 下次启动时直接执行该脚本即可;
接着分别打开各个文件下的 redis.windows.conf,分别修改如下配置(举例修改6380文件下的redis.window.conf文件):
port 6380 //修改为与当前文件夹名字一样的端口号
appendonly yes //指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。 yes表示:存储方式,aof,将写操作记录保存到日志中
cluster-enabled yes //开启集群模式
cluster-config-file nodes-6380.conf //保存节点配置,自动创建,自动更新(建议命名时加上端口号)
cluster-node-timeout 15000 //集群超时时间,节点超过这个时间没反应就断定是宕机
注意:在修改配置文件这几项配置时,配置项前面不能有空格,否则启动时会报错(参考下面)
其他文件节点 6381~6385也修改相应的节点配置信息和建立启动脚本(略)。
2.下载Ruby并安装:
下载地址:http://railsinstaller.org/en 这里下载的是2.3.3版本:
下载完成后安装,一步步点next知道安装完成(安装时勾选3个选项)
然后对ruby进行配置:
3.构建集群脚本redis-trib.rb
可以打开 https://raw.githubusercontent.com/antirez/redis/unstable/src/redis-trib.rb 然后复制里面的内容到本地并保存为redis-trib.rb;
如下图,与redis集群节点保存在同一个文件夹下(比如我所有节点都存放在redis-cluster文件夹下)。
然后依次启动所有集群节点start.bat
然后cmd进入redis集群节点目录后,执行: (–replicas 1 表示为集群中的每个主节点创建一个从节点)
redis-trib.rb create --replicas 1 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385
将会出现下图的输出信息
上图可看出 主节点为6380,6381,6382 端口的三个地址,6384,6385,6383为三个从节点
中途会询问是否打印更多详细信息,输入yes即可,然后redis-trib 就会将这份配置应用到集群当中,让各个节点开始互相通讯
Redis集群数据分配策略:
采用一种叫做哈希槽 (hash slot)的方式来分配数据,redis cluster 默认分配了 16384 个slot,三个节点分别承担的slot 区间是:(上图3个M:节点的slots描述)
节点6380覆盖0-5460;
节点6381覆盖5461-10922;
节点6382覆盖10923-16383.
最后查看所有集群节点,会看到:
集群搭建并启动成功。。。
4.测试集群
进入任意一个集群节点,cmd执行 redis-cli.exe -c -p 6381
写入任意一个value,查询
写一个hash:
hset redis:test:hash Hash1 12345
可以看到集群会用CRC16算法来取模得到所属的slot,然后将这个key分到哈希槽区间的节点上CRC16(key) % 16384
所以,可以看到我们set的key计算之后被分配到了slot-162 上, 而slot-162处在节点6380上,因此redis自动redirect到了6380节点上。
八、订阅与发布
using StackExchange.Redis; using System; namespace ConsoleApp1 { class Program { static void Main(string[] args) { var redisconn = ConnectionMultiplexer.Connect(ConfigurationOptions.Parse("127.0.0.1:6379,password='',connectTimeout=60")); #region 订阅 Console.WriteLine("请输入您要订阅哪个通道的信息?"); var channelKey = Console.ReadLine(); redisconn.GetSubscriber().Subscribe(channelKey, (chanel, msg) => { Console.WriteLine($"通道:{channelKey}接受到发布的内容为:{msg}"); }); Console.WriteLine("您订阅的通道为:<< " + channelKey + " >> ! 一切就绪,等待发布消息!勿动,一动就没啦!!"); #endregion #region 发布 Console.WriteLine("请输入要发布向哪个通道?"); var channel = Console.ReadLine(); Console.WriteLine("请输入要发布的消息内容."); var message = Console.ReadLine(); redisconn.GetSubscriber().Publish(channel, message); #endregion Console.ReadKey(); } } }
封装:
using Newtonsoft.Json; using StackExchange.Redis; using System; using System.Threading; namespace ConsoleApp1 { class Program { static void Main(string[] args) { #region 订阅 var redis = new Redis(); redis.RedisSubMessageEvent += RedisSubMessageEvent; redis.Use(1).RedisSub("redis_20200105_pay"); #endregion #region 发布 for (var i = 1; i < 20; i++) { Redis.Using(rd => { rd.Use(1).RedisPub<string>("redis_20200105_pay", "pay amt=" + i); }); Thread.Sleep(1000); } #endregion Console.ReadKey(); } private static void RedisSubMessageEvent(string msg) { Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} RedisSubMessageEvent: {msg}"); } } public class Redis : IDisposable { private static ConnectionMultiplexer redis = null; private static bool connected = false; private IDatabase db = null; private int current = 0; public static bool IsConnected { get { Open(); return redis.IsConnected; } } public static bool Test() { bool r = true; try { Redis.Using(rs => { rs.Use(0); }); } catch (Exception e) { r = false; } return r; } private static int Open() { if (connected) return 1; redis = ConnectionMultiplexer.Connect("127.0.0.1:6379,password='',abortConnect = false"); connected = true; return 1; } public static void Using(Action<Redis> a) { using (var red = new Redis()) { a(red); } } public Redis Use(int i) { Open(); current = i; db = redis.GetDatabase(i); return this; } public void Set(string key, string val, TimeSpan? ts = null) { db.StringSet(key, val, ts); } public string Get(string key) { return db.StringGet(key); } public void Remove(string key) { db.KeyDelete(key, CommandFlags.HighPriority); } public bool Exists(string key) { return db.KeyExists(key); } public void Dispose() { db = null; } #region Redis发布订阅 public delegate void RedisDeletegate(string str); public event RedisDeletegate RedisSubMessageEvent; /// <summary> /// 订阅 /// </summary> /// <param name="subChannel"></param> public void RedisSub(string subChannel) { redis.GetSubscriber().Subscribe(subChannel, (channel, message) => { RedisSubMessageEvent?.Invoke(message); //触发事件 }); } /// <summary> /// 发布 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="channel"></param> /// <param name="msg"></param> /// <returns></returns> public long RedisPub<T>(string channel, T msg) { string s = JsonConvert.SerializeObject(msg); return redis.GetSubscriber().Publish(channel, JsonConvert.SerializeObject(msg)); } /// <summary> /// 取消订阅 /// </summary> /// <param name="channel"></param> public void Unsubscribe(string channel) { redis.GetSubscriber().Unsubscribe(channel); } /// <summary> /// 取消全部订阅 /// </summary> public void UnsubscribeAll() { redis.GetSubscriber().UnsubscribeAll(); } #endregion } }
将redis发布订阅模式用做消息队列和rabbitmq的区别:
可靠性
redis :没有相应的机制保证消息的可靠消费,如果发布者发布一条消息,而没有对应的订阅者的话,这条消息将丢失,不会存在内存中;
rabbitmq:具有消息消费确认机制,如果发布一条消息,还没有消费者消费该队列,那么这条消息将一直存放在队列中,直到有消费者消费了该条消息,以此可以保证消息的可靠消费;
实时性
redis:实时性高,redis作为高效的缓存服务器,所有数据都存在在服务器中,所以它具有更高的实时性
消费者负载均衡:
rabbitmq队列可以被多个消费者同时监控消费,但是每一条消息只能被消费一次,由于rabbitmq的消费确认机制,因此它能够根据消费者的消费能力而调整它的负载;
redis发布订阅模式,一个队列可以被多个消费者同时订阅,当有消息到达时,会将该消息依次发送给每个订阅者;
持久性
redis:redis的持久化是针对于整个redis缓存的内容,它有RDB和AOF两种持久化方式,可以将整个redis实例持久化到磁盘,以此来做数据备份,防止异常情况下导致数据丢失。
rabbitmq:队列,消息都可以选择性持久化,持久化粒度更小,更灵活;
队列监控
rabbitmq实现了后台监控平台,可以在该平台上看到所有创建的队列的详细情况,良好的后台管理平台可以方面我们更好的使用;
redis没有所谓的监控平台。
总结
redis: 轻量级,低延迟,高并发,低可靠性;
rabbitmq:重量级,高可靠,异步,不保证实时;
rabbitmq是一个专门的AMQP协议队列,他的优势就在于提供可靠的队列服务,并且可做到异步,而redis主要是用于缓存的,redis的发布订阅模块,可用于实现及时性,且可靠性低的功能。
九、Redis五种数据类型的使用场景
1、字符串(Strings)
缓存功能:
字符串最经典的使用场景,redis具有支撑高并发特性,所以缓存通常能起到加速读写和降低IO操作的作用。
计数器:
许多运用都会使用redis作为计数的基础工具,他可以实现快速计数、查询缓存的功能,同时数据可以一步落地到其他的数据源。
如:视频播放数系统就是使用redis作为视频播放数计数的基础组件。
共享session:
出于负载均衡的考虑,分布式服务会将用户信息的访问均衡到不同服务器上,
用户刷新一次访问可能会需要重新登录,为避免这个问题可以用redis将用户session集中管理,
在这种模式下只要保证redis的高可用和扩展性的,每次获取用户更新或查询登录信息
都直接从redis中集中获取。
限速:
出于安全考虑,每次进行登录时让用户输入手机验证码,为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率。
database.StringSet("name", "苍");//设置StringSet(key, value) string str = database.StringGet("name");//结果:苍 database.StringSet("name_two", str, TimeSpan.FromSeconds(10));//设置时间,10s后过期。
//创建对象 Demo demo = new Demo() { Name = "苍", Age = 18, Height = 1.83 }; string demojson = JsonConvert.SerializeObject(demo);//序列化 database.StringSet("model", demojson); string model = database.StringGet("model"); demo = JsonConvert.DeserializeObject<Demo>(model);//反序列化
2、哈希(Hashes)
存储、读取、修改用户属性
常用于存储一个对象(用户姓名、年龄、生日......)
使用string增加了序列号反序列化的开销,key重复值太多内存的浪费
var user = new User { Name = "james", Age = 18 }; string json = JsonConvert.SerializeObject(user);//序列化 db.HashSet("user", "londo", json); db.HashSet("user", "polo", json); db.HashSet("user", "kobe", json); //获取Model string hashcang = db.HashGet("user", "londo"); user = JsonConvert.DeserializeObject<User>(hashcang);//反序列化 //获取List RedisValue[] values = db.HashValues("user");//获取所有value IList<User> demolist = new List<User>(); foreach (var item in values) { User hashmodel = JsonConvert.DeserializeObject<User>(item); demolist.Add(hashmodel); }
3、列表(Lists)
1).最新消息排行等功能(比如朋友圈的时间线)
2).消息队列
3).文章列表:
每个用户都有属于自己的文章列表,现在需要分页展示文章列表,此时可以考虑使用列表,列表不但有序
同时支持按照索引范围获取元素
for (int i = 0; i < 6; i++) { db.ListRightPush("list", i);//从底部插入数据 } for (int i = 6; i < 10; i++) { db.ListLeftPush("list", i);//从顶部插入数据 } long length = db.ListLength("list");//长度 10 RedisValue rightPop = db.ListRightPop("list");//从底部拿出数据 即:删除5 var popValue = rightPop.ToString();// 5 RedisValue leftpop = db.ListLeftPop("list");//从顶部拿出数据 即:删除9 RedisValue[] list = db.ListRange("list");//列表数据 8 7 6 0 1 2 3 4 var s = list[0].ToString(); // 8
4、集合(Sets)
1).共同好友
2).利用唯一性,统计访问网站的所有独立ip
3).好友推荐时,根据tag求交集,大于某个阈值就可以推荐
标签(tag):
集合类型比较典型的使用场景,如一个用户对娱乐、体育比较感兴趣,另一个可能对新闻感兴趣,
这些兴趣就是标签,有了这些数据就可以得到同一标签的人,以及用户的共同爱好的标签,
这些数据对于用户体验以及曾强用户粘度比较重要。
db.SetAdd("TagAsNBA", "1张三"); db.SetAdd("TagAsNBA", "2李四"); db.SetAdd("TagAsNBA", "3王五"); RedisValue[] setList = db.SetMembers("TagAsNBA"); foreach (var item in setList) { Console.WriteLine(item); }
5、有序集合(Sorted sets)
1).排行榜
2).带权重的消息队列
排行榜:
有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,
榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。
db.SortedSetAdd("RankingAsScore", "张三", 1); db.SortedSetAdd("RankingAsScore", "王五", 3); db.SortedSetAdd("RankingAsScore", "李四", 2); RedisValue[] sortedSetList = db.SortedSetRangeByRank("RankingAsScore"); foreach (RedisValue t in sortedSetList) { Console.WriteLine(t + " --- " + t.HasValue); }
测试源码:
using Newtonsoft.Json; using StackExchange.Redis; using System; using System.Collections.Generic; using System.Threading; namespace ConsoleApp1 { class Program { public class User { public string Name { get; set; } public int Age { get; set; } } #region ConnectionMultiplexer是StackExchange.Redis的核心,它被整个应用程序共享和重用,应该设置为单例 // redis config // 哨兵26379 主从服务器6379,6380,开启哨兵模式,会自动链接 private static readonly ConfigurationOptions ConfigurationOptions = ConfigurationOptions.Parse("127.0.0.1:6379,password='',connectTimeout=60"); //the lock for singleton private static readonly object Locker = new object(); //singleton private static ConnectionMultiplexer _redisConn; //singleton public static ConnectionMultiplexer GetRedisConn() { if (_redisConn == null) { lock (Locker) { if (_redisConn == null || !_redisConn.IsConnected) { _redisConn = ConnectionMultiplexer.Connect(ConfigurationOptions); } } } return _redisConn; } #endregion static void Main(string[] args) { _redisConn = GetRedisConn(); IDatabase db = _redisConn.GetDatabase(1); #region sets 集合 db.SetAdd("TagAsNBA", "1张三"); db.SetAdd("TagAsNBA", "2李四"); db.SetAdd("TagAsNBA", "3王五"); RedisValue[] setList = db.SetMembers("TagAsNBA"); foreach (var item in setList) { Console.WriteLine(item); } #endregion #region SetSorted 有序集合 db.SortedSetAdd("RankingAsScore", "张三", 1); db.SortedSetAdd("RankingAsScore", "王五", 3); db.SortedSetAdd("RankingAsScore", "李四", 2); RedisValue[] sortedSetList = db.SortedSetRangeByRank("RankingAsScore"); foreach (RedisValue t in sortedSetList) { Console.WriteLine(t + " --- " + t.HasValue); } #endregion #region hash ////创建对象 //var user = new User //{ // Name = "james", // Age = 18 //}; //string json = JsonConvert.SerializeObject(user);//序列化 //db.HashSet("user", "londo", json); //db.HashSet("user", "polo", json); //db.HashSet("user", "kobe", json); ////获取Model //string hashcang = db.HashGet("user", "londo"); //user = JsonConvert.DeserializeObject<User>(hashcang);//反序列化 ////获取List //RedisValue[] values = db.HashValues("user");//获取所有value //IList<User> demolist = new List<User>(); //foreach (var item in values) //{ // User hashmodel = JsonConvert.DeserializeObject<User>(item); // demolist.Add(hashmodel); //} #endregion #region list //for (int i = 0; i < 6; i++) //{ // db.ListRightPush("list", i);//从底部插入数据 //} //for (int i = 6; i < 10; i++) //{ // db.ListLeftPush("list", i);//从顶部插入数据 //} //long length = db.ListLength("list");//长度 10 //RedisValue rightPop = db.ListRightPop("list");//从底部拿出数据 即:删除5 //var popValue = rightPop.ToString();// 5 //RedisValue leftpop = db.ListLeftPop("list");//从顶部拿出数据 即:删除9 //RedisValue[] list = db.ListRange("list");//列表数据 8 7 6 0 1 2 3 4 //var s = list[0].ToString(); // 8 #endregion //var strKey = "hello"; //var strValue = "world"; //db.StringSet(strKey, strValue, TimeSpan.FromSeconds(500)); #region 订阅 //var redis = new Redis(); //redis.RedisSubMessageEvent += RedisSubMessageEvent; //redis.Use(1).RedisSub("redis_20200105_pay"); #endregion #region 发布 //for (var i = 1; i < 20; i++) //{ // Redis.Using(rd => { // rd.Use(1).RedisPub<string>("redis_20200105_pay", "pay amt=" + i); // }); // Thread.Sleep(200); //} #endregion Console.ReadKey(); } private static void RedisSubMessageEvent(string msg) { Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} RedisSubMessageEvent: {msg}"); } } public class Redis : IDisposable { private static ConnectionMultiplexer redis = null; private static bool connected = false; private IDatabase db = null; private int current = 0; public static bool IsConnected { get { Open(); return redis.IsConnected; } } public static bool Test() { bool r = true; try { Redis.Using(rs => { rs.Use(0); }); } catch (Exception e) { r = false; } return r; } private static int Open() { if (connected) return 1; redis = ConnectionMultiplexer.Connect("127.0.0.1:6379,password='',abortConnect = false"); connected = true; return 1; } public static void Using(Action<Redis> a) { using (var red = new Redis()) { a(red); } } public Redis Use(int i) { Open(); current = i; db = redis.GetDatabase(i); return this; } public void Set(string key, string val, TimeSpan? ts = null) { db.StringSet(key, val, ts); } public string Get(string key) { return db.StringGet(key); } public void Remove(string key) { db.KeyDelete(key, CommandFlags.HighPriority); } public bool Exists(string key) { return db.KeyExists(key); } public void Dispose() { db = null; } #region Redis发布订阅 public delegate void RedisDeletegate(string str); public event RedisDeletegate RedisSubMessageEvent; /// <summary> /// 订阅 /// </summary> /// <param name="subChannel"></param> public void RedisSub(string subChannel) { redis.GetSubscriber().Subscribe(subChannel, (channel, message) => { RedisSubMessageEvent?.Invoke(message); //触发事件 }); } /// <summary> /// 发布 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="channel"></param> /// <param name="msg"></param> /// <returns></returns> public long RedisPub<T>(string channel, T msg) { string s = JsonConvert.SerializeObject(msg); return redis.GetSubscriber().Publish(channel, JsonConvert.SerializeObject(msg)); } /// <summary> /// 取消订阅 /// </summary> /// <param name="channel"></param> public void Unsubscribe(string channel) { redis.GetSubscriber().Unsubscribe(channel); } /// <summary> /// 取消全部订阅 /// </summary> public void UnsubscribeAll() { redis.GetSubscriber().UnsubscribeAll(); } #endregion } }
十、Redis分布式锁秒杀场景案例
第一问,有没有用过分布式锁?
有,基于redis的分布锁
第二问,redis为什么可以做分布式锁?
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。
代码实现的,主要是针对某一笔数据的流水号加锁,防止多个线程写入这个数据。(具有互斥性)
源码:
using Newtonsoft.Json; using StackExchange.Redis; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp1 { class Program { static void Main(string[] args) { const string productKey = "0002_RC";//秒杀key hotelId_RoomType Thread.Sleep(5000); // 2、创建20个请求来秒杀 for (var i = 0; i < 20; i++) { var thread = new Thread(() => { SkillProduct(productKey); }); thread.Start(); } Console.ReadKey(); } /// <summary> /// 秒杀方法 /// </summary> public static void SkillProduct(string productKey) { var redisLock = new RedisLock(); redisLock.Lock(Environment.MachineName); // 1、获取商品库存 var stockNum = redisLock.GetStockNum(productKey); // 2、判断商品库存是否为空 if (stockNum == 0) { // 2.1 秒杀失败消息 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:不好意思,秒杀已结束,商品编号:{stockNum}"); redisLock.UnLock(Environment.MachineName); return; } // 3、秒杀成功消息 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:恭喜你,秒杀成功,商品编号:{stockNum}"); // 4、扣减商品库存 redisLock.SubStockNum(productKey); redisLock.UnLock(Environment.MachineName); } } /// <summary> /// redis分布式锁 /// 分布式锁四要素 /// 1、锁名 /// 2、加锁操作 /// 3、解锁操作 /// 4、锁超时时间 /// </summary> public class RedisLock { // 1、redis连接管理类 private readonly ConnectionMultiplexer _connectionMultiplexer = null; // 2、redis数据操作类 private readonly IDatabase _database; public RedisLock() { _connectionMultiplexer = ConnectionMultiplexer.Connect("127.0.0.1:6379"); _database = _connectionMultiplexer.GetDatabase(0); } /// <summary> /// 加锁 /// 1、key : 锁名 /// 2、value : 谁加了这把锁 : 防止锁被其线程释放掉 /// 3、超时时间 :防止死锁 /// </summary> public void Lock(string key) { //如果加锁失败,继续获取锁,无限失败 while (true) { //当前服务器id,可以用酒店ID var value = Environment.MachineName;//Thread.CurrentThread.ManagedThreadId // 1、key:锁名,可以用酒店id或者商品id 2、value:谁加了这把锁,防止锁被其线程释放掉 3、超时时间:防止死锁 bool isSucceed = _database.LockTake(key, value, TimeSpan.FromSeconds(10)); if (isSucceed) { break; } // 休眠一下 Thread.Sleep(200); } } /// <summary> /// 解锁 /// </summary> public void UnLock(string key) { // 1、解锁 _database.LockRelease(key, Environment.MachineName); // 2、关闭资源 _connectionMultiplexer.Close(); } /// <summary> /// 获取库存 /// </summary> public int GetStockNum(string key) { var redisValue = _database.StringGet(key); var num = JsonConvert.DeserializeObject<int>(redisValue); return num; } /// <summary> /// 扣减库存 /// </summary> /// <param name="key"></param> /// <returns></returns> public void SubStockNum(string key) { var redisValue = _database.StringGet(key); var num = JsonConvert.DeserializeObject<int>(redisValue); var value = num - 1; _database.StringSet(key, value); } } }
使用c#的Lock为什么不行?
引文Lock只能锁线程不能锁进程。
源码:
using Newtonsoft.Json; using StackExchange.Redis; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp1 { class Program { static void Main(string[] args) { const string productKey = "0002_RC";//秒杀key hotelId_RoomType Thread.Sleep(5000); ////创建30个请求来秒杀 //for (var i = 0; i < 30; i++) //{ // var thread = new Thread(() => // { // SkillProduct(productKey); // }); // thread.Start(); //} //创建30个请求来秒杀 var clock = new CsharpLock(); for (var i = 0; i < 30; i++) { var thread = new Thread(() => { clock.SkillProduct(productKey); }); thread.Start(); } Console.ReadKey(); } /// <summary> /// 秒杀方法 /// </summary> public static void SkillProduct(string productKey) { var redisLock = new RedisLock(); redisLock.Lock(Environment.MachineName); // 1、获取商品库存 var stockNum = redisLock.GetStockNum(productKey); // 2、判断商品库存是否为空 if (stockNum == 0) { // 2.1 秒杀失败消息 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:不好意思,秒杀已结束,商品编号:{stockNum}"); redisLock.UnLock(Environment.MachineName); return; } // 3、秒杀成功消息 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:恭喜你,秒杀成功,商品编号:{stockNum}"); // 4、扣减商品库存 redisLock.SubStockNum(productKey); redisLock.UnLock(Environment.MachineName); } } /// <summary> /// C#Lock锁 /// </summary> public class CsharpLock { public readonly object LockObj = new object(); /// <summary> /// 秒杀方法 /// </summary> public void SkillProduct(string productKey) { var redisLock = new RedisLock(); lock (LockObj) { // 1、获取商品库存 var stockNum = redisLock.GetStockNum(productKey); // 2、判断商品库存是否为空 if (stockNum == 0) { // 2.1 秒杀失败消息 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:不好意思,秒杀已结束,商品编号:{stockNum}"); redisLock.UnLock(Environment.MachineName); return; } // 3、秒杀成功消息 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:恭喜你,秒杀成功,商品编号:{stockNum}"); // 4、扣减商品库存 redisLock.SubStockNum(productKey); } } } /// <summary> /// redis分布式锁 /// 分布式锁四要素 /// 1、锁名 /// 2、加锁操作 /// 3、解锁操作 /// 4、锁超时时间 /// </summary> public class RedisLock { // 1、redis连接管理类 private readonly ConnectionMultiplexer _connectionMultiplexer = null; // 2、redis数据操作类 private readonly IDatabase _database; public RedisLock() { _connectionMultiplexer = ConnectionMultiplexer.Connect("127.0.0.1:6379"); _database = _connectionMultiplexer.GetDatabase(0); } /// <summary> /// 加锁 /// 1、key : 锁名 /// 2、value : 谁加了这把锁 : 防止锁被其线程释放掉 /// 3、超时时间 :防止死锁 /// </summary> public void Lock(string key) { //如果加锁失败,继续获取锁,无限失败 while (true) { //当前服务器id,可以用酒店ID var value = Environment.MachineName;//Thread.CurrentThread.ManagedThreadId // 1、key:锁名,可以用酒店id或者商品id 2、value:谁加了这把锁,防止锁被其线程释放掉 3、超时时间:防止死锁 bool isSucceed = _database.LockTake(key, value, TimeSpan.FromSeconds(10)); if (isSucceed) { break; } // 休眠一下 Thread.Sleep(200); } } /// <summary> /// 解锁 /// </summary> public void UnLock(string key) { // 1、解锁 _database.LockRelease(key, Environment.MachineName); // 2、关闭资源 _connectionMultiplexer.Close(); } /// <summary> /// 获取库存 /// </summary> public int GetStockNum(string key) { var redisValue = _database.StringGet(key); var num = JsonConvert.DeserializeObject<int>(redisValue); return num; } /// <summary> /// 扣减库存 /// </summary> public void SubStockNum(string key) { var redisValue = _database.StringGet(key); var num = JsonConvert.DeserializeObject<int>(redisValue); var value = num - 1; _database.StringSet(key, value); } } }
Lock(this)有问题
源码:
public bool LockTake(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) { if (value.IsNull) throw new ArgumentNullException(nameof(value)); return StringSet(key, value, expiry, When.NotExists, flags); }
可以看到调用的是set方法,继续看set方法可以看到
private Message GetStringSetMessage(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) { WhenAlwaysOrExistsOrNotExists(when); if (value.IsNull) return Message.Create(Database, flags, RedisCommand.DEL, key); if (expiry == null || expiry.Value == TimeSpan.MaxValue) { // no expiry switch (when) { case When.Always: return Message.Create(Database, flags, RedisCommand.SET, key, value); case When.NotExists: return Message.Create(Database, flags, RedisCommand.SETNX, key, value); case When.Exists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX); } } long milliseconds = expiry.Value.Ticks / TimeSpan.TicksPerMillisecond; if ((milliseconds % 1000) == 0) { // a nice round number of seconds long seconds = milliseconds / 1000; switch (when) { case When.Always: return Message.Create(Database, flags, RedisCommand.SETEX, key, seconds, value); case When.Exists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX); case When.NotExists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.NX); } } switch (when) { case When.Always: return Message.Create(Database, flags, RedisCommand.PSETEX, key, milliseconds, value); case When.Exists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX); case When.NotExists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.NX); } throw new NotSupportedException(); }
RedisCommand.SETNX:不执行set命令如果key存在
RedisCommand.SETEX:命令将覆盖已有的值
XX
: 只在键已经存在时, 才对键进行设置操作