优点:
1)成本:nosql数据库简单易部署,基本都是开源软件,不需要像使用oracle那样花费大量成本购买使用,相比关系型数据库价格便宜。
2)查询速度:nosql数据库将数据存储于缓存之中,关系型数据库将数据存储在硬盘中,自然查询速度远不及nosql数据库。
3)存储数据的格式:nosql的存储格式是key,value形式、文档形式、图片形式等等,所以可以存储基础类型以及对象或者是集合等各种格式,而数据库则只支持基础类型。
4)扩展性:关系型数据库有类似join这样的多表查询机制的限制导致扩展很艰难。
缺点:
1)维护的工具和资料有限,因为nosql是属于新的技术,不能和关系型数据库10几年的技术同日而语。
2)不提供对sql的支持,如果不支持sql这样的工业标准,将产生一定用户的学习和使用成本。
3)不提供关系型数据库对事务的处理。
关系型数据库与NoSQL数据库并非对立而是互补的关系,即通常情况下使用关系型数据库,在适合使用NoSQL的时候使用NoSQL数据库,让NoSQL数据库对关系型数据库的不足进行弥补。一般会将数据存储在关系型数据库中,在nosql数据库中备份存储关系型数据库的数据
• 键值(Key-Value)存储数据库
相关产品: Tokyo Cabinet/Tyrant、Redis、Voldemort、Berkeley DB
典型应用: 内容缓存,主要用于处理大量数据的高访问负载。
数据模型: 一系列键值对
优势: 快速查询
劣势: 存储的数据缺少结构化
• 列存储数据库
相关产品:Cassandra, HBase, Riak
典型应用:分布式的文件系统
数据模型:以列簇式存储,将同一列数据存在一起
优势:查找速度快,可扩展性强,更容易进行分布式扩展
劣势:功能相对局限
• 文档型数据库
相关产品:CouchDB、MongoDB
典型应用:Web应用(与Key-Value类似,Value是结构化的)
数据模型: 一系列键值对
优势:数据结构要求不严格
劣势: 查询性能不高,而且缺乏统一的查询语法
• 图形(Graph)数据库
相关数据库:Neo4J、InfoGrid、Infinite Graph
典型应用:社交网络
数据模型:图结构
优势:利用图结构相关算法。
劣势:需要对整个图做计算才能得出结果,不容易做分布式的集群方案。
Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。50个并发执行100000个请求,读的速度是110000次/s,写的速度是81000次/s ,且Redis通过提供多种键值数据类型来适应不同场景下的存储需求。
redis 内置16个数据库
切换:select (0~15)
测试语句
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
Redis是很快的,官方表示,Redis是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络带
宽,既然可以使用单线程来实现,就使用单线程了!所有就使用了单线程了!
Redis 是C语言写的,官方提供的数据为100000+的QPS,完全不比同样是使用key-vale的Memecache差!
Redis为什么单线程还这么快?
误区一:高性能服务器一定是多线程!
误区二:多线程(CPU上下文会切换!)一定比单线程效率高!
先去CPU>内存>硬盘的速度要有所了解!
核心:redis是将所有的数据全部放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗
没有操作!!!),对于内存系统来说,如果没有上下文切换效率就是最高的!多次读写都是在一个CPU上的,在内存情况这个就是最佳的方案!
- redis存储的是:key,value格式的数据,其中key都是字符串,value有5种不同的数据结构
- value的数据结构:
- 字符串类型 string
2) 哈希类型 hash : map格式
3) 列表类型 list : linkedlist格式。支持重复元素
4) 集合类型 set : 不允许重复元素
5) 有序集合类型 sortedset:不允许重复元素,且元素有顺序
- 计数器
string类型的incr和decr命令的作用是将key中储存的数字值加一/减一,这两个操作具有原子性,总能安全地进行加减操作,因此可以用string类型进行计数,如微博的评论数、点赞数、分享数,抖音作品的收藏数,京东商品的销售量、评价数等。
2. 分布式锁
string类型的setnx的作用是“当key不存在时,设值并返回1,当key已经存在时,不设值并返回0”,“判断key是否存在”和“设值”两个操作是原子性地执行的,因此可以用string类型作为分布式锁,返回1表示获得锁,返回0表示没有获得锁。例如,为了保证定时任务的高可用,往往会同时部署多个具备相同定时任务的服务,但是业务上只希望其中的某一台服务执行定时任务,当定时任务的时间点触发时,多个服务同时竞争一个分布式锁,获取到锁的执行定时任务,没获取到的放弃执行定时任务。定时任务执行完时通过del命令删除key即释放锁,如果担心del命令操作失败而导致锁一直未释放,可以通过expire命令给锁设置一个合理的自动过期时间,确保即使del命令失败,锁也能被释放。不过expire命令同样存在失败的可能性,如果你用的是Java语言,建议使用JedisCommands接口提供的String set(String key, String value, String nxxx, String expx, long time)方法,这个方法可以将setnx和expire原子性地执行,具体使用方式如下(相信其它语言的Redis客户端也应当提供了类似的方法)。
jedisCommands.set("IAmAKey", "1", "NX", "EX", 60);//如果"IAmAKey"不存在,则将其设值为1,同时设置60秒的自动过期时间
- 存储对象
利用JSON强大的兼容性、可读性和易用性,将对象转换为JSON字符串,再存储在string类型中,是个不错的选择,如用户信 息、商品信息等。
string类型的常用命令可参考http://www.runoob.com/redis/redis-strings.html。
select 3 #切换至3号数据库
dbsize #查看当前数据库的 数据量 一般为 k-v 的对数
keys * #查看当前库中的所有 key
flushdb #清空当前数据库
flushall #清空所有数据库
set name zz #存入一个 name-zz的 k-v数据
get name #将返回这个key对应的值 zz
exits name #判断当前key是否存在 存在返回1否则返回0
move name 1 #移动 这个k-v 到指定数据库
expire name 10 #设置这个k-v的过期时间为10秒
ttl name #查看这个k-v剩余的有效期时间
type name #查看当前key的类型
append name "hh" #将name对应的value值拼接 再次get name时会返回 zzhh 如果key不存在则等价于set
strlen name #查看这个key对应的value值的长度
set views 0
incr views #自增1 下次get views将返回1
decr views #自减1 下次get views将又返回0
incrby views 5 #自增5 下次get views将返回5
decrby views 5 #自减5 下次get views将返回0
getrange name 1 2 #返回name对应value的一部分 从下标1开始到下标2结束 即zh
getrange name 0 -1 #返回name对应value的一部分 从下标0开始到末尾 即zzhh
setrange name 1 ab #将name下标1开始的位置 替换为ab 返回 zabh
setex name1 10 "aaa" #如果name1不存在则创建k-v并指定过期时间10秒,具有原子性。 如果存在则覆盖value指定过期时间10秒
setnx name2 bbb #如果不存在这个key则创建成功返回1,如果存在 则创建失败返回0
mset k1 v1 k2 v2 k3 v3 #批量设置这3个k-v对
mget k1 k2 k3 #批量返回相应k值的相应value值
msetnx k1 v1 k4 v4 #批量不存在时设置,具有原子性,如此时k1存在k4不存在,但会全部失败返回0
set user:1 {name:zhangsan,age:10} #保存对象
mset user:1:name zhangsan user:1:ange 10 #也可有上述效果
getset name ccc #先get再set 不存在时 返回nil,但set仍然会生效!等价与创建了一个新的k-v。存在时返回之前的value值且覆盖掉这个value,下次get会返回刚设置的新值
list类型是简单的字符串列表,按照插入顺序排序。每个列表最多可以存储 232 - 1 个元素(40多亿) ,list类型主要有以下应用场景。。
1. 消息队列
list类型的lpop和rpush(或者反过来,lpush和rpop)能实现队列的功能,故而可以用Redis的list类型实现简单的点对点的消息队列。不过我不推荐在实战中这么使用,因为现在已经有Kafka、NSQ、RabbitMQ等成熟的消息队列了,它们的功能已经很完善了,除非是为了更深入地理解消息队列,不然我觉得没必要去重复造轮子。
2. 排行榜
list类型的lrange命令可以分页查看队列中的数据。可将每隔一段时间计算一次的排行榜存储在list类型中,如京东每日的手机销量排行、学校每次月考学生的成绩排名、斗鱼年终盛典主播排名等,下图是酷狗音乐“K歌擂台赛”的昨日打擂金曲排行榜,每日计算一次,存储在list类型中,接口访问时,通过page和size分页获取打擂金曲。但是,并不是所有的排行榜都能用list类型实现,只有定时计算的排行榜才适合使用list类型存储,与定时计算的排行榜相对应的是实时计算的排行榜,list类型不能支持实时计算的排行榜,之后在介绍有序集合sorted set的应用场景时会详细介绍实时计算的排行榜的实现。
- 最新列表
list类型的lpush命令和lrange命令能实现最新列表的功能,每次通过lpush命令往列表里插入新的元素,然后通过lrange命令读取最新的元素列表,如朋友圈的点赞列表、评论列表。
但是,并不是所有的最新列表都能用list类型实现,因为对于频繁更新的列表,list类型的分页可能导致列表元素重复或漏掉,举个例子,当前列表里由表头到表尾依次有(E,D,C,B,A)五个元素,每页获取3个元素,用户第一次获取到(E,D,C)三个元素,然后表头新增了一个元素F,列表变成了(F,E,D,C,B,A),此时用户取第二页拿到(C,B,A),元素C重复了。只有不需要分页(比如每次都只取列表的前5个元素)或者更新频率低(比如每天凌晨更新一次)的列表才适合用list类型实现。对于需要分页并且会频繁更新的列表,需用使用有序集合sorted set类型实现。另外,需要通过时间范围查找的最新列表,list类型也实现不了,也需要通过有序集合sorted set类型实现,如以成交时间范围作为条件来查询的订单列表。之后在介绍有序集合sorted set类型的应用场景时会详细介绍sorted set类型如何实现最新列表。
#list的操作
lpush list one #往list顶端插入值
lrange list 0 -1 #返回list中所有值
rpush list four #往list底部插入值
lpop list #移除list顶端元素,并返回该元素
rpop list #移除list底部元素,并返回该元素
lindex list 0 #获取list中指定下标为0的值,同上理解就是顶端
llen list #返回list的长度
lrem list 1 value #移除list中指定个数的value值
ltrim list 1 2 #截断list,保留指定下标中的值
rpoplpush list1 haha2 #从list1底部移除一个元素并返回,且将该元素插入list2的顶端
exists list #判断list是否存在 存在返回1否则返回0
lset list 0 haha #修改列表指定位置的值, 需要列表和该位置元素已存在,否则报错
linsert list before hello new #往指定列表的指定元素的前面插入指定值
linsert list after hello new1 #往指定列表的指定元素的后面插入指定值
- 好友/关注/粉丝/感兴趣的人集合
set类型唯一的特点使得其适合用于存储好友/关注/粉丝/感兴趣的人集合,集合中的元素数量可能很多,每次全部取出来成本不小,set类型提供了一些很实用的命令用于直接操作这些集合,如
a. sinter命令可以获得A和B两个用户的共同好友
b. sismember命令可以判断A是否是B的好友
c. scard命令可以获取好友数量
c. 关注时,smove命令可以将B从A的粉丝集合转移到A的好友集合
需要注意的是,如果你用的是Redis Cluster集群,对于sinter、smove这种操作多个key的命令,要求这两个key必须存储在同一个slot(槽位)中,否则会报出 (error) CROSSSLOT Keys in request don’t hash to the same slot 错误。Redis Cluster一共有16384个slot,每个key都是通过哈希算法CRC16(key)获取数值哈希,再模16384来定位slot的。要使得两个key处于同一slot,除了两个key一模一样,还有没有别的方法呢?答案是肯定的,Redis提供了一种Hash Tag的功能,在key中使用{}括起key中的一部分,在进行 CRC16(key) mod 16384 的过程中,只会对{}内的字符串计算,例如friend_set:{123456}和fans_set:{123456},分别表示用户123456的好友集合和粉丝集合,在定位slot时,只对{}内的123456进行计算,所以这两个集合肯定是在同一个slot内的,当用户123456关注某个粉丝时,就可以通过smove命令将这个粉丝从用户123456的粉丝集合移动到好友集合。相比于通过srem命令先将这个粉丝从粉丝集合中删除,再通过sadd命令将这个粉丝加到好友集合,smove命令的优势是它是原子性的,不会出现这个粉丝从粉丝集合中被删除,却没有加到好友集合的情况。然而,对于通过sinter获取共同好友而言,Hash Tag则无能为力,例如,要用sinter去获取用户123456和456789两个用户的共同好友,除非我们将key定义为{friend_set}:123456和{friend_set}:456789,否则不能保证两个key会处于同一个slot,但是如果真这样做的话,所有用户的好友集合都会堆积在同一个slot中,数据分布会严重不均匀,不可取,所以,在实战中使用Redis Cluster时,sinter这个命令其实是不适合作用于两个不同用户对应的集合的(同理其它操作多个key的命令)。
- 随机展示
通常,app首页的展示区域有限,但是又不能总是展示固定的内容,一种做法是先确定一批需要展示的内容,再从中随机获取。如下图所示,酷狗音乐K歌擂台赛当日的打擂歌曲共29首,首页随机展示5首;昨日打擂金曲共200首,首页随机展示30首。
set类型适合存放所有需要展示的内容,而srandmember命令则可以从中随机获取几个。
- 黑名单/白名单
经常有业务出于安全性方面的考虑,需要设置用户黑名单、ip黑名单、设备黑名单等,set类型适合存储这些黑名单数据,sismember命令可用于判断用户、ip、设备是否处于黑名单之中。
# set操作(无序不重复集合)
sadd myset zhangsan lisi SMEMBERS myset # set集合中添加值
SMEMBERS myset # 查看set集合的所有值
SISMEMBER myset lisi # 判断set集合中是否有这个值
scard myset # 获取set集合中的元素个数
srem myset lisi # 删除set集合指定的元素
SRANDMEMBER myset # 随机抽取出一个元素
SRANDMEMBER myset 2 # 随机抽选出2个元素
spop myset # 随机删除set集合的元素
spop myset 2 # 随机删除set集合的2ge元素
SDIFF myset myset2 # 两个set集合的差集
SINTER myset myset2 # 两个set集合的交集
SUNION myset myset2 # 两个set集合的并集
Map集合, key-map!时候这个值是一个map集合 !本质和string没有太大区别,还是一个简单的key-value!
hash类型是一个string类型的field和value的映射表,每个 hash 可以存储 232 - 1 键值对(40多亿),hash类型主要有以下应用场景。
- 购物车
以用户id为key,商品id为field,商品数量为value,恰好构成了购物车的3个要素。
- 存储对象
hash类型的(key, field, value)的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
在介绍string类型的应用场景时有所介绍,string + json也是存储对象的一种方式,那么存储对象时,到底用string + json还是用hash呢?
两种存储方式的对比如下表所示。
string + json hash 效率 很高 高 容量 低 低 灵活性 低 高 序列化 简单 复杂 当对象的某个属性需要频繁修改时,不适合用string+json,因为它不够灵活,每次修改都需要重新将整个对象序列化并赋值,如果使用hash类型,则可以针对某个属性单独修改,没有序列化,也不需要修改整个对象。比如,商品的价格、销量、关注数、评价数等可能经常发生变化的属性,就适合存储在hash类型里。
当然,不常变化的属性存储在hash类型里也没有问题,比如商品名称、商品描述、上市日期等。但是,当对象的某个属性不是基本类型或字符串时,使用hash类型就必须手动进行复杂序列化,比如,商品的标签是一个标签对象的列表,商品可领取的优惠券是一个优惠券对象的列表(如下图所示)等,即使以coupons(优惠券)作为field,value想存储优惠券对象列表也还是要使用json来序列化,这样的话序列化工作就太繁琐了,不如直接用string + json的方式存储商品信息来的简单。
# hash操作
#hash变更的数据user name age,尤其是是用户信息之类的,经常变动的信息! hash更适合于对象的存储, String更加适合字符串存储!
hset myhash k1 v1 # 设置一个key-value
hmget k1 k2 # 根据多个key获取value
hmset myhash l1 v2 l3 v4 # 设置多个key-value
hgetall myhash # 查询hash表的所有值
hdel myhash k1 # 删除指定的field
hlen myhash # 获取hash表的field数量
HEXISTS myhash k2 # 判断hash表中是否存在这个field
hkeys myhash # 只获得hash表中的所有field
HVALS myhash # 只获得hash表中的所有value
HINCRBY myhash k1 1 #自增1
hsetnx myhash k1 v1 # 如果不存在这个key则创建成功返回1,如果存在 则创建失败返回0
- 延时队列
zset 会按 score 进行排序,如果 score 代表想要执行时间的时间戳。在某个时间将它插入zset集合中,它变会按照时间戳大小进行排序,也就是对执行时间前后进行排序。
起一个死循环线程不断地进行取第一个key值,如果当前时间戳大于等于该key值的score就将它取出来进行消费删除,可以达到延时执行的目的。
- 排行榜
经常浏览技术社区的话,应该对 “1小时最热门” 这类榜单不陌生。如何实现呢?如果记录在数据库中,不太容易对实时统计数据做区分。我们以当前小时的时间戳作为 zset 的 key,把贴子ID作为 member ,点击数评论数等作为 score,当 score 发生变化时更新 score。利用 ZREVRANGE 或者 ZRANGE 查到对应数量的记录。
- 限流
滑动窗口是限流常见的一种策略。如果我们把一个用户的 ID 作为 key 来定义一个 zset ,member 或者 score 都为访问时的时间戳。我们只需统计某个 key 下在指定时间戳区间内的个数,就能得到这个用户滑动窗口内访问频次,与最大通过次数比较,来决定是否允许通过。
# Zset操作
zadd sarly 5000 wangwu # 添加操作
ZRANGE sarly 0 -1 # 输出zset的所有值
ZRANGEBYSCORE sarly -inf +inf # 升序输出全部的用户
ZRANGEBYSCORE sarly -inf +inf withscores # 序输出全部的用户,且显示工资
ZRANGEBYSCORE sarly -inf +inf 2500 withscores # 序输出全部的用户,且显示大于2500的工资
ZREVRANGEBYSCORE sarly +inf -inf # 降序排序
zrem sarly lisi # 删除用户
zcard sarly # 获取有序集合的元素个数
ZREVRANGE sarly 0 -1 # 反转
zcount sarly 500 2500 # 获取指定区间的元素的数
geospatial常用命令
GEOADD
# getadd 添加地理位置
#规则:两级无法直接添加,我们一般会下载城市数据,直接通过java程序一 次性导入!
#有效的经度从-180度到180度。
#有效的纬度从-85. 05112878度到85.05112878度。
#当坐标位置超出上述指定范围时,该命令将会返回一个错误。
GEOADD china:cipty 31.405 121.4894 shanghai # 添加地理位置
GEODIST
geodist china:cipty beijing shanghai km # 获取两个地理之间的距离,单位是km
GEOHASH
geohash china:cipty beijing shanghai # 将二维的经纬度转换为-维的字符串,如果两个字符串越接近,那么则距离越近!
GEOPOS
geopos china:cipty beijing #获取指定的城市的经度和纬度!
GEORADIUS
GEORADIUS china:cipty 110 30 1000 km # georadius以给定的经纬度为中心,找出某一半径内的元素
GEORADIUSBYMEMBER
GEORADIUSBYMEMBER china:cipty beijing 10000 km # 找出位于指定元素周围的其他元素!
总结
GEO底层的实现原理其实就是Zset !我们可以使用Zset命令来操作geo !
- 删除
- 查询
- …等操作
应用场景
说明:
- 基数不大,数据量不大就用不上,会有点大材小用浪费空间
- 有局限性,就是只能统计基数数量,而没办法去知道具体的内容是什么
- 和bitmap相比,属于两种特定统计情况,简单来说,HyperLogLog 去重比 bitmap 方便很多
- 一般可以bitmap和hyperloglog配合使用,bitmap标识哪些用户活跃,hyperloglog计数
一般使用:
- 统计注册 IP 数
- 统计每日访问 IP 数
- 统计页面实时 UV 数
- 统计在线用户数
- 统计用户每天搜索不同词条的个数
PFadd mykey aIa b C defghij #创建第一组元素mykey
PFCOUNT mykey # 统计mykey元素的基数数量
PFadd mykey2 i j z xcvbnm #创建第二组元素mykey2
PFMERGE mykey3 mykey mykey2 #合并两组mykey mykey2 => mykey3 并集
PFCOUNT mykey3 #看并集的数量!
如果允许容错,那么-定可以使用Hyperloglog !
如果不允许容错,就使用Iset或者自己的数据类型即可!
使用场景一:用户签到
很多网站都提供了签到功能(这里不考虑数据落地事宜),并且需要展示最近一个月的签到情况.
使用场景二:统计活跃用户
使用时间作为cacheKey,然后用户ID为offset,如果当日活跃过就设置为1那么我该如果计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只有有一天在线就称为活跃),有请下一个redis的命令命令
BITOP operation destkey key [key ...]
说明:对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。使用场景三:用户在线状态
bitmap是一个节约空间效率又高的一种方法,只需要一个key,然后用户ID为offset,如果在线就设置为1,不在线就设置为0.
SETBIT sig 0 1 # 设置位图
GETBIT sig 6 # 获取位图
################################
`用户签到`:模拟用户打卡场景,0-6为一周,0为未打卡,1为打卡
BITCOUNT sig # 统计用户一周打卡天数