说明:本文中涉及的代码是c#所写,连接redis的第三方驱动为ServiceStack.Redis。连接redis的客户端软件为redis-desktop-manager。
一、Redis是什么
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
Redis是一种内存数据库,是一种NoSQL数据库。
简单概括:
1. 是一个完全开源免费的key-value内存数据库
2. 通常被认为是一个数据结构服务器,主要是因为其有着丰富的数据结构 strings、map、 list、sets、 sorted sets
NoSQL数据库的四大分类:
键值(Key-Value)存储数据库
这一类数据库主要会使用到一个哈希表,这个表中有一个特定的键和一个指针指向特定的数据。Key/value模型对于IT系统来说的优势在于简单、易部署。但是如果DBA只对部分值进行查询或更新的时候,Key/value就显得效率低下了。 举例如:Tokyo Cabinet/Tyrant, Redis, Voldemort, Oracle BDB.
列存储数据库。
这部分数据库通常是用来应对分布式存储的海量数据。键仍然存在,但是它们的特点是指向了多个列。这些列是由列家族来安排的。如:Cassandra, HBase, Riak.
文档型数据库
文档型数据库的灵感是来自于Lotus Notes办公软件的,而且它同第一种键值存储相类似。该类型的数据模型是版本化的文档,半结构化的文档以特定的格式存储,比如JSON。文档型数据库可 以看作是键值数据库的升级版,允许之间嵌套键值。而且文档型数据库比键值数据库的查询效率更高。如:CouchDB, MongoDb. 国内也有文档型数据库SequoiaDB,已经开源。
图形(Graph)数据库
图形结构的数据库同其他行列以及刚性结构的SQL数据库不同,它是使用灵活的图形模型,并且能够扩展到多个服务器上。NoSQL数据库没有标准的查询语言(SQL),因此进行数据库查询需要制定数据模型。许多NoSQL数据库都有REST式的数据接口或者查询API。 如:Neo4J, InfoGrid, Infinite Graph.
因此,我们总结NoSQL数据库在以下的这几种情况下比较适用:
1、数据模型比较简单;
2、需要灵活性更强的IT系统;
3、对数据库性能要求较高;
4、不需要高度的数据一致性;
5、对于给定key,比较容易映射复杂值的环境。
Redis与Memcached
网络IO模型方面:Memcached是多线程,分为监听线程、worker线程,引入锁,带来了性能损耗。Redis使用单线程的IO复用模型,将速度优势发挥到最大,也提供了较简单的计算功能
内存管理方面:Memcached使用预分配的内存池的方式,带来一定程度的空间浪费 并且在内存仍然有很大空间时,新的数据也可能会被剔除,而Redis使用现场申请内存的方式来存储数据,不会剔除任何非临时数据 Redis更适合作为存储而不是cache
数据的一致性方面:Memcached提供了cas命令来保证.而Redis提供了事务的功能,可以保证一串 命令的原子性,中间不会被任何操作打断
存储方式方面:Memcached只支持简单的key-value存储,不支持枚举,不支持持久化和复制等功能
Redis与关系数据库
redis目前还只能作为小数据量存储(全部数据能够加载在内存中) ,海量数据存储方面并不是redis所擅长的领域。
设计、实现方法很不一样.关系型数据库通过表来存储数据,通过SQL来查询数据。而Redis通上述五种数据结构来存储数据,通过命令来查询数据。
二、为什么要用Redis
1. 速度快:使用标准C写,所有数据都在内存中完成,读写速度分别达到10万/20万
2. 持久化:对数据的更新采用Copy-on-write技术,可以异步地保存到磁盘上,主要有两种策略,一是根据时间,更新次数的快照(save 300 10 )二是基于语句追加方式(Append-only file,aof)
3.集中式缓存管理,统一缓存
4. 自动操作:对不同数据类型的操作都是自动的,很安全
5. 快速的主--从复制,官方提供了一个数据,Slave在21秒即完成了对Amazon网站10G key set的复制。
6. Sharding技术: 很容易将数据分布到多个Redis实例中,数据库的扩展是个永恒的话题,在关系型数据库中,主要是以添加硬件、以分区为主要技术形式的纵向扩展解决了很多的应用场景,但随着web2.0、移动互联网、云计算等应用的兴起,这种扩展模式已经不太适合了,所以近年来,像采用主从配置、数据库复制形式的,Sharding这种技术把负载分布到多个特理节点上去的横向扩展方式用处越来越多。
三、Redis的应用场景
1.取最新N个数据的操作
比如典型的取你网站的最新文章,通过下面方式,我们可以将最新的5000条评论的ID放在Redis的List集合中,并将超出集合部分从数据库获取
使用LPUSH latest.comments<ID>命令,向list集合中插入数据
插入完成后再用LTRIM latest.comments 0 5000命令使其永远只保存最近5000个ID
如果你还有不同的筛选维度,比如某个分类的最新N条,那么你可以再建一个按此分类的List,只存ID的话,Redis是非常高效的。
2.排行榜应用,取TOP N操作
这个需求与上面需求的不同之处在于,前面操作以时间为权重,这个是以某个条件为权重,比如按顶的次数排序,这时候就需要我们的sorted set出马了,将你要排序的值设置成sorted set的score,将具体的数据设置成相应的value,每次只需要执行一条ZADD命令即可。
3.需要精准设定过期时间的应用
比如你可以把上面说到的sorted set的score值设置成过期时间的时间戳,那么就可以简单地通过过期时间排序,定时清除过期数据了,不仅是清除Redis中的过期数据,你完全可以把Redis里这个过期时间当成是对数据库中数据的索引,用Redis来找出哪些数据需要过期删除,然后再精准地从数据库中删除相应的记录。
4.计数器应用
Redis的命令都是原子性的,你可以轻松地利用INCR,DECR命令来构建计数器系统。
5.Uniq操作,获取某段时间所有数据排重值
这个使用Redis的set数据结构最合适了,只需要不断地将数据往set中扔就行了,set意为集合,所以会自动排重。
6.实时系统,反垃圾系统
通过上面说到的set功能,你可以知道一个终端用户是否进行了某个操作,可以找到其操作的集合并进行分析统计对比等。没有做不到,只有想不到。
7.Pub/Sub构建实时消息系统
Redis的Pub/Sub系统可以构建实时的消息系统,比如很多用Pub/Sub构建的实时聊天系统的例子。
8.构建队列系统
使用list可以构建队列系统,使用sorted set甚至可以构建有优先级的队列系统。
9.缓存
这个不必说了,性能优于Memcached,数据结构更多样化。
四、Redis示例
Redis主要支持类型
String
Hash
List
Set
Sorted set
public static T GetIfNullFunc<T>(string redisIP, int redisPort, string key, Func<T> func) where T : class, new() { using (RedisClient rclient = new RedisClient(redisIP, redisPort)) { var cacheData = ser.Deserialize(rclient.Get<byte[]>(key)) as T; if (cacheData == null) { cacheData = func(); if (cacheData != null) rclient.Set<byte[]>(key, ser.Serialize(cacheData)); } return (T)cacheData; } }
带过期时间
public static T GetIfNullFunc<T>(string redisIP, int redisPort, int redisSeconds, string key, Func<T> func) where T : class, new() { using (RedisClient rclient = new RedisClient(redisIP, redisPort)) { var cacheData = ser.Deserialize(rclient.Get<byte[]>(key)) as T; if (cacheData == null) { cacheData = func(); if (cacheData != null) rclient.Set<byte[]>(key, ser.Serialize(cacheData), TimeSpan.FromSeconds(redisSeconds)); } return (T)cacheData; } }
key-value存取示例
string cacheKey = string.Format("CNKI_OKMS_Category_Unit_{0}", unitId); return RedisHelper.GetIfNullFunc<List<Entity.CategoryInfo>>(CoreConfig.Instance.RedisIP, CoreConfig.Instance.RedisPort, CoreConfig.Instance.RedisSeconds, cacheKey, () => { return _categoryDal.GetCategorys(unitId).ToList<Entity.CategoryInfo>(); });
string
RedisClient rclient = new RedisClient("192.168.103.69", 6379); rclient.Set<int>("z1", 6); rclient.Set<string>("z2", "hello world"); rclient.Set<double>("z3", 22.09); rclient.Set<int>("IpAccessCount", 0); //次数递增 rclient.Incr("IpAccessCount"); Console.WriteLine(rclient.Get<int>("IpAccessCount")); Student z4 = RedisHelper.GetIfNullFunc<Student>("192.168.103.69", "z4", () => { return new Student() { Name = "zhangsan", age = 22, email = "zhangsan@123.com" }; });
远程查看内存数据库(使用软件redis-desktop-manager查看)
Hash
rclient.Remove("hash1"); rclient.SetEntryInHash("hash1", "key1", "6.0"); rclient.SetEntryInHash("hash1", "key2", "value2"); rclient.SetEntryInHash("hash1", "key3", "value3"); rclient.IncrementValueInHash("hash1", "key1", 2.0); List<string> keys = rclient.GetHashKeys("hash1"); List<string> values = rclient.GetHashValues("hash1"); Dictionary<string, string> dic = rclient.GetAllEntriesFromHash("hash1");
List
应用:
实现最新消息排行等功能。
Lists的另一个应用就是消息队列,可以利用Lists的PUSH操作,将任务存在Lists中,然后工作线程再用POP操作将任务取出进行执行。
client.AddItemToList("userInfoId1", "123"); client.AddItemToList("userInfoId1", "1234"); Console.WriteLine("List数据项条数:" + client.GetListCount("userInfoId1")); Console.WriteLine("List数据项第一条数据:" + client.GetItemFromList("userInfoId1", 0)); Console.WriteLine("List所有数据"); client.GetAllItemsFromList("userInfoId1").ForEach(e => Console.WriteLine(e)); #region "List类型做为队列和栈使用" Console.WriteLine(client.GetListCount("userInfoId1")); //队列先进先出 Console.WriteLine(client.DequeueItemFromList("userInfoId1")); Console.WriteLine(client.DequeueItemFromList("userInfoId1")); //栈后进先出 Console.WriteLine("出栈"+client.PopItemFromList("userInfoId1")); Console.WriteLine("出栈"+client.PopItemFromList("userInfoId1")); #endregion
set
应用场景:
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的。
比如在微博应用中,每个人的好友存在一个集合(set)中,这样求两个人的共同好友的操作,可能就只需要用求交集命令即可。
Redis还为集合提供了求交集、并集、差集等操作。
client.AddItemToSet("A", "B"); client.AddItemToSet("A", "C"); client.AddItemToSet("A", "D"); client.AddItemToSet("A", "E"); client.AddItemToSet("A", "F"); client.AddItemToSet("B", "C"); client.AddItemToSet("B", "F"); //求差集 Console.WriteLine("A,B集合差集"); client.GetDifferencesFromSet("A", "B").ToList<string>().ForEach(e => Console.Write(e + ",")); //求集合交集 Console.WriteLine(" A,B集合交集"); client.GetIntersectFromSets(new string[] { "A", "B" }).ToList<string>().ForEach(e => Console.Write(e + ",")); //求集合并集 Console.WriteLine(" A,B集合并集"); client.GetUnionFromSets(new string[] { "A", "B" }).ToList<string>().ForEach(e => Console.Write(e + ","));
SortedSet
应用场景:
以某个条件为权重,比如按项的次数排序.
ZREVRANGE命令可以用来按照得分来获取前100名的用户,ZRANK可以用来获取用户排名,非常直接而且操作容易。
Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。
比如:twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。
比如:全班同学成绩的SortedSets,value可以是同学的学号,而score就可以是其考试得分,这样数据插入集合的,就已经进行了天然的排序。
另外还可以用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
client.AddItemToSortedSet("SA", "B", 2); client.AddItemToSortedSet("SA", "C", 1); client.AddItemToSortedSet("SA", "D", 5); client.AddItemToSortedSet("SA", "E", 3); client.AddItemToSortedSet("SA", "F", 4); //有序集合降序排列 Console.WriteLine(" 有序集合降序排列"); client.GetAllItemsFromSortedSetDesc("SA").ForEach(e => Console.Write(e + ",")); Console.WriteLine(" 有序集合升序序排列"); client.GetAllItemsFromSortedSet("SA").ForEach(e => Console.Write(e + ",")); client.AddItemToSortedSet("SB", "C", 2); client.AddItemToSortedSet("SB", "F", 1); client.AddItemToSortedSet("SB", "D", 3); Console.WriteLine(" 获得某个值在有序集合中的排名,按分数的升序排列"); Console.WriteLine(client.GetItemIndexInSortedSet("SB", "D")); Console.WriteLine(" 获得有序集合中某个值得分数"); Console.WriteLine(client.GetItemScoreInSortedSet("SB", "D")); Console.WriteLine(" 获得有序集合中,某个排名范围的所有值"); client.GetRangeFromSortedSet("SA", 0, 3).ForEach(e => Console.Write(e + ","));
五、主从
过程: 数据写到master-->master存储到slave的rdb中-->slave加载rdb到内存。
存储点(save point): 当网络中断了, 连上之后, 继续传.
Master-slave下第一次同步是全传,后面是增量同步;
主(Master)
如果用的是redis的windows服务,修改配置文件redis.windows-service.conf;如果用redis-server.exe在命令行启动,修改配置文件redis.windows.conf
port 6379(可以自行修改)
bind 192.168.103.69
其他参数:
设置master密码,slave连接master时需要输入密码
masterauth <master-password>
设置数据库个数,默认写入db0(第一个数据库)
databases 16
保存数据到磁盘:
save 900 1 修改1到10个key,15分钟后保存到磁盘
save 300 10 修改10到10000个key,5分钟后保存到磁盘
save 60 10000 修改10000个key以上,1分钟后保存到磁盘
从1(slave1)与master主机相同,端口不同
修改redis.windows.conf配置文件
port 6385
bind 192.168.103.69
slaveof 192.168.103.69 6379
从2(slave2)
修改redis.windows-service.conf
port 6379
bind 192.168.25.246
slaveof 192.168.103.69 6379
先重启master上的redis服务
然后重启slave上的redis服务
master日志:
[3424] 12 Aug 03:33:00.015 * Slave 192.168.25.246:6379 asks for synchronization
[3424] 12 Aug 03:33:00.016 * Full resync requested by slave 192.168.25.246:6379
[3424] 12 Aug 03:33:00.017 * Starting BGSAVE for SYNC with target: disk
[3424] 12 Aug 03:33:00.047 * Background saving started by pid 3408
[3424] 12 Aug 03:33:00.208 # fork operation complete
[3424] 12 Aug 03:33:00.385 * Background saving terminated with success
[3424] 12 Aug 03:33:00.388 * Synchronization with slave succeeded
slave2日志:
[28016] 11 Aug 19:39:18.705 * Connecting to MASTER 192.168.103.69:6379
[28016] 11 Aug 19:39:18.705 * MASTER <-> SLAVE sync started
[28016] 11 Aug 19:39:18.707 * Non blocking connect for SYNC fired the event.
[28016] 11 Aug 19:39:18.716 * Master replied to PING, replication can continue...
[28016] 11 Aug 19:39:18.726 * Partial resynchronization not possible (no cached master)
[28016] 11 Aug 19:39:18.729 * Full resync from master: ab7f739c4ed4c62d4941a455e9f5706153efe1e8:1
[28016] 11 Aug 19:39:19.100 * MASTER <-> SLAVE sync: receiving 1086 bytes from master
[28016] 11 Aug 19:39:19.102 * MASTER <-> SLAVE sync: Flushing old data
[28016] 11 Aug 19:39:19.102 * MASTER <-> SLAVE sync: Loading DB in memory
[28016] 11 Aug 19:39:19.102 * MASTER <-> SLAVE sync: Finished with success
主从同步情况查看
读写分离访问
private static string[] ReadWriteHosts = System.Configuration.ConfigurationSettings.AppSettings["readWriteHosts"].Split(new char[] { ';' }); private static string[] ReadOnlyHosts = System.Configuration.ConfigurationSettings.AppSettings["readOnlyHosts"].Split(new char[] { ';' }); public static PooledRedisClientManager prcm = CreateManager(ReadWriteHosts, ReadOnlyHosts); private static PooledRedisClientManager CreateManager(string[] readWriteHosts, string[] readOnlyHosts) { // 支持读写分离,均衡负载 return new PooledRedisClientManager(readWriteHosts, readOnlyHosts, new RedisClientManagerConfig { MaxWritePoolSize = 5, // “写”链接池链接数 MaxReadPoolSize = 5, // “读”链接池链接数 AutoStart = true, }); } /// <summary> /// 移除单体 /// </summary> /// <param name="key"></param> public static bool Item_Remove(string key) { using (IRedisClient redis = prcm.GetClient()) { return redis.Remove(key); } } public static int List_Count(string key) { using (IRedisClient redis = prcm.GetReadOnlyClient()) { return redis.GetListCount(key); } }
日志和慢查询日志
查询日志
loglevel notice
logfile "Logs/redis_log.txt"
慢查询日志
slowlog-log-slower-than 10000(单位:微秒)
slowlog-max-len 128 (队列长度)
六、发布订阅(消息系统)
简单pub_sub
七、消息队列
RabbitMQ是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。个人认为,在互联网开发中,使用消息队列,更多的因为在高并发环境下,由于来不及同步处理,请求会发生堵塞,所以我们需要一个队列服务来进行异步的处理,在这种场景下,只要队列服务满足最基本的Push/Pop已经足够了。
Redis是一个Key-Value的NoSQL数据库,开发维护很活跃,虽然它是一个Key-Value数据库存储系统,但它本身支持list数据结构的操作,所以完全可以当做一个轻量级的队列服务来使用。
经验与教训
1. 要进行Master-slave配置,出现服务故障时可以支持切换。
2. 在master侧禁用数据持久化,只需在slave上配置数据持久化。
3. 物理内存+虚拟内存不足,这个时候dump一直死着,时间久了机器挂掉。这个情况就是灾难
4. 当Redis物理内存使用超过内存总容量的3/5时就会开始比较危险了,就开始做swap,内存碎片大
5. 当达到最大内存时,会清空带有过期时间的key,即使key未到过期时间.
6. redis与DB同步写的问题,先写DB,后写redis,因为写内存基本上没有问题
参考资料
1.Redis book(Redis设计与实现) http://redisbook.readthedocs.org/en/latest/
2.Redis 官网 http://redis.io/
3.http://devres.zoomquiet.io/data/20110714104018/index.html
4.ServiceStack源码
5.NServiceKit源码