一、背景(功能转向性能)
技术的分类:
- 解决功能性的问题:Java,Jsp,Tomcat,HTML,Linux,JDBC,RDBMS
- 解决扩展性的问题:Struts,Spring,SpringMVC,Hibernate,Mybatis
- 解决性能的问题:NOSQL,Java线程,Hadoop,Nginx
背景:
(1)web1.0的时代,数据访问量很有限,用一夫当关的高性能单点服务器可以解决大部分问题。
(2)随和web2.0的时代到来,用户访问量大幅度提升,同时产生了大量的用户数据,加上后来的智能移动设备的普及,所有的互联网平台都面临了巨大的性能挑战。
(3)以session存在哪儿的示例,展示NoSQL数据库解决服务器CPU内存压力,和IO压力
二、非关系型数据库(NOSQL)
概述:
- NoSQL(Not Only SQL),泛指非关系型数据库,NoSQL不依赖业务逻辑方式存储,而以简单的key-value模式存储,因此大大增加了数据库的扩展能力。
- 不遵循SQL标准
- 不支持ACID
- 远超于SQL的性能
NoSQL适用场景:
- 对数据高并发的读写
- 海量数据的读写
- 对数据高可扩展性的
NoSQL不适用场景:
- 需要事务支持
- 基于SQL的结构化查询存储,处理复杂的关系,需要条件查询
类型:
- 键值存储数据库:这一类数据库主要会使用一个哈希表,这个表中有一个特定的键和一个指针指向特定的数据。如Redis。
- 列存储数据库:这部分数据库通常是用来应对分布式存储的海量数据。键仍然存在,但是它们的特点是指向了多个列,如HBase。
- 文档型数据库:该类型的数据模型是版本化的文档,半结构化的文档以特定的格式存储,比如JSON。文档型数据库可以看作是键值数据库的升级版,允许之间嵌套键值,而且文档型数据库比键值数据库的查询效率更高,如MongoDb。
- 图形数据库:图形结构的数据库同其他行列以及刚性结构的SQL数据库不同,它是使用灵活的图形模型,并且能够扩展到多个服务器上,非关系型数据库没有标准的查询语言,因此进行数据库查询需要制定数据模型,许多非关系型数据库都有REST式的数据接口或者查询 API。
Redis简介:
Redis是以key-value形式存储,和传统的关系型数据库不一样,不一定遵循传统数据库的一些基本要求。
Redis是以key-value形式存储,键可以包含:字符串(String),哈希,链表(list),集合(set),有序集合(zset)。这些数据结合都支持push/pop,add/remove以及取交集和并集以及更丰富的操作,Redis支持各种不同的方式排序,为了保证效率,数据都是缓存在内存汇总,它也可以周期性的把更新的数据写入磁盘或者把修改操作写入追加到文件中。
优点:
- 对数据高并发读写
- 对海量数据的高效率存储和访问
- 对数据的可扩展性和高可用性
缺点:
- Redis(ACID处理非常简单),无法做到太复杂的关系数据库模型
Redis是一个开源的key-value存储系统,和Memcached类似,它支持存储的value类型相对更多,包括String(字符串),list(链表),set(集合),zset(有序集合)和hash(哈希类型)。这些是数据类型都支持push/pop,add/remove以及取交集并集和差集,以及更丰富的操作,而且这些操作都是原子性的。在此基础上,Redis支持各种不同方式的排序,与Memcached一样,为了保证效率,数据都是缓存在内存中,区别是Redis会周期性的把更新的数据写入磁盘或者把修改操作追加到记录文件,并且在此基础上实现了主从同步。
Redis应用场景:
(1)配合关系型数据库做高速缓存
- 高频词,热门访问的数据,降低数据库I/O
- 分布式架构,做session共享
(2)由于其拥有持久化能力,利用其多样的数据结构存储特定的数据。
Redis是单线程+多路IO复用技术:
IO多路复用机制:
IO操作包括:IO准备阶段和IO操作阶段。IO多路复用机制类似下图,不停检测多个IO是否准备就绪,类似于拨开关,谁有数据就拨向谁,然后调用相应的代码来处理,这样不会因为IO等待阶段阻塞整个过程,效率较高。
三、Redis五大数据类型(本质都是存储字符串)
(0)对key的操作
key * 查询当前库的所有键
exists <key> 判断某个键是否存在
type <key> 查看键的类型
del <key> 删除某个键
expire <key> <seconds> 为键值设置过期时间,单位为秒
ttl <key> 查看还有多少秒过期,-1代表永不过期,-2代表已过期
dbsize 查看当前数据库的key的数量
Flushdb 清空当前库
Flushall 通杀全部库
(1)string类型
- String是Redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value
- String类型是二进制安全的,意味着Redis的String可以包含任何数据,比如jpg图片或者序列化的对象
- String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M
常见指令:
get <key> 查询键对应的值
set <key> <value> 添加键值对
append <key> <value> 将给定的value追加到原值的末尾
strlen <key> 获取值的长度
setnx <key> <value> 只有在key不存在时,设置key值
incr <key> 将key中存储的数字值增1,只能对数字值操作,如果为空,新增值为1
decr <key> 将key中存储的数字值减1,只能对数字值操作,如果为空,新增至为-1
incrby/decrby <key> <步长> 将key中存储的数字值增减,自定义步长
mset <key1> <value1> <key2> <value2>... 同时设置一个或多个key-value对
mget <key1> <key2> <key3>... 同时获取一个或多个value
msetnx <key1> <value1> <key2> <value2>... 同时设置一个多个key-value对,并且仅当所有给定key都不存在
getrange <key> <起始位置> <结束位置> 获得值的范围,类似java中的substring
setrange <key> <起始位置> <value> 用value覆写key所存储的字符串值,从起始位置开始
setex <key> <过期时间> <value> 设置键值的同时,设置过期时间,单位为秒
getset <key> <value> 以新换旧,设置了新值同时获得旧值
注意:
- 原子性:所谓原子性是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何切换到另一个线程的行为。
- 在单线程中,能够在单条指令中完成的操作都可以认为是源自操作,因为中断只能发生于指令之间。
- 在多线程中,不能被其他线程打断的操作就叫原子操作。
Redis单命令的原子性主要得益于Redis的单线程!!!
(2)list类型
- 单键多值
- Redis列表是简单的字符串列表,按照插入顺序,你可以添加一个元素导列表的头部(左边)或尾部(右边)
- 它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差
常见指令:
lpush/rpush <key> <value1> <value2>... 从左边/右边插入一个或多个值
lpop/rpop <key> 从左边/右边吐出一个值,值在键在,值光键亡
rpoplpush <key1> <key2> 从key1列表右边吐出一个值,查到key2列表的左边
lrange <key> <start> <stop> 按照索引下标获得元素(从左到右)
lindex <key> <index> 按照索引小标获得元素(从左到右)
llen <key> 获得列表长度
linsert <key> before <value> <newvalue> 在value的前面插入newvalue插入值
linsert <key> after <value> <newvalue> 在value的后面插入newvalue插入值
lrem <key> <n> <value> 从左边删除n个value(从左到右)
(3)set类型
- Redis set对外提供的功能与list类似,是一个列表的功能。特殊之处在于set是可以自动排除重复的,当你需要存储一个列表数据,又不需要出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set结合内的重要接口,这个也是list所不能提供的。
- Redis的set是string类型的无序集合,它底层其实是一个valule为null的哈希表,所以添加,删除,查找的复杂度都是O(1)
常见指令:
sadd <key> <value1> <value2> ... 将一个或多个member元素加入到集合key中,已经存在于集合的member元素将被忽略
smembers <key> 取出该集合的所有值
sismember <key> <value> 判断集合key是否含有该value值,若有则返回1,否则返回0
scard <key> 返回该集合的元素个数
srem <key> <value1> <value2> ... 删除集合中的某个元素
spop <key> 随机从该集合中吐出一个值
srandmember <key> <n> 随机从该集合中取出n个值,不会从集合中删除
sinter <key1> <key2> 返回两个集合的交际元素
sunion <key1> <key2> 返回两个集合的并集元素
sdiff <key1> <key2> 返回两个集合的差集元素
(4)hash类型
- Redis hash是一个键值对集合
- Redis hash是一个string类型的field和value的映射表,hash特别适合用户存储对象
- 类似Java里面的Map<String,String>
常见指令:
hset <key> <field> <value> 给key集合中的field键赋值value
hget <key1> <field> 从key1集合field中取出value
hmset <key1> <field1> <value1> <field2> <value2>... 批量设置hash值
hexists key <field> 查看哈希表key中,给定域field是否存在
hkeys <key> 列出该hash集合的所有field
hvals <key> 列出该hash集合的所有value
hincrby <key> <field> <increment> 为哈希表key中的域field的值加上增量increment
hsetnx <key> <field> <value> 将哈希表key中的域field的值设置为value,当且仅当field不存在
(5)zset类型(相当于以元素为键,以分数为值的map)
- Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。不同的是有序集合的每个成员都关联一个评分(score),这个score被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复的。
- 因为元素是有序的,所有你也可以很快根据评分(score)或者次序(position)来获取一个范围的元素。访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
常见指令:
zadd <key> <score1> <value1> <score2> <value2>... 讲一个或多个member元素及其score值加入到有序集key当中
zrange <key> <start> <stop> [withscores] 返回有序集key中,下标在start和stop之间的元素,带withscores,可以让分数一起和值返回到结果集
zrangebyscore key min max [withscores] [limit offset count] 返回有序集key中,所有score值介于min和max之间(包括等于min或max)的成员。有序集成员按score值递增(从小到大)次序排列
zrevrangebyscore key max min [withscores] [limit offset count] 同上,改为从大到小排列
zincrby <key> <increment> <value> 为元素的score加上增量
zrem <key> <value> 删除该集合下,指定值的元素
zcount <key> <min> <max> 统计该集合,分数区间内的元素个数
zrank <key> <value> 返回该值在集合中的排名,从0开始
四、Java连接Redis(Redis的Java客户端Jedis)
在Java中使用Redis,首先需要导入Redis的jar包
public class Test{
public static void main(String [] args){
//创建Redis对象
Jedis jedis = new Jedis("192.168.44.132",6379);
//测试Redis是否连接成功
String result=jedis.ping();
System.out.println(result); //连接成功,返回PONG
//测试在Redis中添加一个键值对,并返回
jedis.set("a","abc");
String str=jedis.get("a");
System.out.println(str);
jedis.close();
}
}
五、Redis的事务
- Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化,按顺序地执行。事务在执行过程中,不会被其他客户端发送来的命令请求所打断。
- Redis事务的主要作用就是串联多个命令防止别的命令插队。(批量执行指令的功能)
- Multi:开启事务
- Exec:执行事务
- discard:取消事务
- 事务的错误处理:组队中的某个命令出现报告错误,执行时整个的所有队列会都被取消。如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
事务的冲突问题:
- 悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都会认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据都会block直到它拿到锁。传统的关系型数据库里面就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在做操作之前先上锁。
- 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
Redis事务的三个特性:
- 单独的隔离操作:事务中的所有命令都会序列化,按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念
- 不保证原子性:Redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
Redis事务---秒杀案例:(考虑性能问题)
由于秒杀的特性,用户只能秒杀一次,所有使用set数据类型存储秒杀成功的用户,即保证每个用户只能秒杀一次。
//uid:用户id
//pid:商品id
public static boolean test(String uid,String pid){
//该kucunKey键,对应一个String值,存储商品的库存
String kucunKey="Seckill:"+pid+":kucun";
//该userKey键,对应一个set集合,存储秒杀成功的用户id
String userKey="Seckill:"+pid+":kucun";
Jedis jedis=new Jedis("192.168.223.132",6379);
//获取库存
String kucun=jedis.get(kucunKey);
//秒杀还没开始,表示为库存为null
if(kucun==null){
System.out.println("秒杀还没开始");
jedis.close();
retrun false;
}
//已经秒杀成功,表示为存储uid的set中已经有该用户uid
//判断userKey键对应的set集合中是否存在该uid
if(jedis.sismember(userKey,uid)){
System.out.println("已经秒杀成功,不可重复秒杀");
jedis.close();
return false;
}
//判断库存,若大于0,则减库存,加入,若小于等于0,则秒杀结束
if(Integer.parseInt(kucun)<=0){
System.out.println("秒杀已经结束");
jedis.close();
return false;
}else{
jedis.decr(kucunKey); //通过jedis减库存
jedis.sadd(userKey,uid); //通过jedis在秒杀成功的用户set中加人
System.out.println("秒杀成功");
jedis.close();
return true;
}
}
六、Redis的持久化
由于Redis是做缓存来使用的,是非关系型数据库,把数据存在内存中,所以需要使用持久化技术。
Redis提供了两种不同形式的持久化方式:
- RDB(Redis DataBase):在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时试将快照文件直接读到内存中。
- AOF(Append Of File):以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以写文件,Redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写指令从前到后执行一次完成数据的回复工作。
注意:RDB存储的是数据,而AOF存储的是指令
七、Redis主从复制(降低读写压力)
主从复制:
就是主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,其中master以写为主,slaver以读为主。
用处:
- 读写分离,性能扩展
- 容灾快速恢复
复制原理:
- 每次从机联通后,都会给主机发送sync指令
- 主机立刻进行存盘操作,发送RDB文件,给从机
- 从机收到RDB文件后,进行全盘加载
- 之后每次主机的写操作,都会立即发送给从机,从机执行相同的命令
薪火相传:
上一个slave可以是下一个slave的master,slave同样可以接收其他slaves的连接和同步请求,那么该slave作为了链条中下一个的master,可以有效减轻master的写压力,去中心化降低风险。
风险是一旦某个slave宕机,后面的slave都无法备份。
哨兵模式:(基于主从复制)
反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转为主库。
八、Redis集群
背景:
- 容量不够,Redis如何进行扩容
- 并发写操作,Redis如何分摊
集群:
- Redis集群实现了对Redis的水平扩容,即启动N个Redis节点,将整个数据库分布存储到这N个节点中,每个节点存储总数据的1/N。
- Redis集群通过分区来提供一定程度的可用性,即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求。
Redis集群的好处:
- 实现扩容
- 分摊压力
- 无中心配置相对简单
Redis集群的不足:
- 多键操作时不被支持的
- 多键的Redis事务是不被支持的,lua脚本不被支持
- 由于集群方案出现较晚,很多公司已经采用其他的集群方案,而代理或者客户端分片的方案想要迁移至Redis集群,需要整体迁移而不是逐步过渡,复杂度较大。
总结:
什么是Redis?
- Redis是用C语言开发的,且基于高性能键值对(key-value)的内存数据库,是一种NoSQL数据库,可以用作数据库,缓存等。
- Redis作为一种内存数据库库,性能优秀,其数据存储在内存中,读写速度非常快。并且Redis是采用单线程+IO多路复用机制,性能高于Memcached的多线程+锁机制。
- Redis支持丰富的数据类型,包括字符串(String),链表(list),集合(set),哈希(hash),有序集合(zset)等五种数据类型。(本质都是存储字符串)
- Redis支持数据持久化,包括RDB和AOF两种技术,其中RDB在指定的时间间隔将内存中的数据集快照写入磁盘中,AOF则将Redis的每个写操作以日志的形式记录下来。
Redis为什么快?
- Redis是完全基于内存的,绝大部分请求是纯粹的内存操作,非常快速。数据存储在内存中,类似于hashMap,查找和操作的时间复杂度都是O(1)
- Redis的数据结构简单,5中数据类型都是时专门进行设计的
- Redis采用单线程,在计算机内部,对于单核cpu来说,永远都是单线程的,多线程只是通过划分cpu时间片来实现的。使用多线程时,cpu在切换线程的时候,有一个上下文切换时间,在这段时间cpu什么都不干,只是做了保存上下文的动作。而多线程一般用于I/O操作时,因为I/O操作一般分为两个阶段:等待I/O准备就绪和真正操作I/O资源。在等待I/O准备就绪的时间内,线程是阻塞着等待,此时操作系统可以将空闲着的cpu核心用于服务其他线程,因此,多线程多用于I/O操作。而在Redis中数据都位于内存中,不涉及I/O操作,因此使用单线程可以避免上下文切换,也即使用单线程效率更高。
- Redis采用IO多路复用技术,将IO监视过程放在处理请求之前,可以监视用户请求是否准备完毕,如果准备完毕可以直接使用Redis进行处理,而不需要Redis等待的过程。所以使用监视,Redis的工作是一直持续下去的,因此效率较高。
Redis与Memcached的区别?
- Memcached使用简单的字符串类型,而Redis拥有5中数据类型。
- Memcached使用多线程+锁的机制,而Redis使用单线程+IO多路复用技术,效率更高。
- Memcached的数据全部存储在内存中,断电后会挂掉,而Redis支持数据持久化,包括RDB技术和AOF技术。
Redis主从复制的原理和哨兵模式的原理?
为了降低Redis的读写压力,使用主机进行写操作,而使用从机进行读操作,为了保证主机和从机的数据一致性,需要进行主从复制,每次从机联通后,都会给主机发送同步命令,主机立刻进行存盘操作,发送RDB文件给从机,从机接收到RDB文件后,进行全盘加载,并且在之后,每次主机执行写操作后,都会立即发送个从机,并且从机执行相同的命令。
哨兵模式是基于主从复制的,使用哨兵监视主机的存活状态,如果主机挂掉,则采用投票机制选择一台从机作为新的主机。新的主机向原来的从机发送命令,复制新的主机,当之前挂掉的主机重新上线后,也会成为新的从机。