打赏

【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();
        }
    }
}
View Code

 

如果主服务器挂了,从服务器不能立刻变为主库,写数据会失败,因为哨兵切换从库为主库时默认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  
View Code

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    #是否激活重置哈希
View Code

 

一主二从三哨兵

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
    }
}
View Code

将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
    }
}
View Code

 

十、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);
        }

    }
}
View Code

 

 

使用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);
        }

    }
}
View Code

 

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 : 只在键已经存在时, 才对键进行设置操作

 

posted @ 2019-01-23 14:27  cksun  阅读(13381)  评论(0编辑  收藏  举报