1、什么是NOSQL
NOSQL(Not Only Sql)
,意为不仅仅是sql,泛指非关系型数据库。
2、为什么NOSQL
随着互联网的兴起,传统的关系型数据库在应对动态网站,特别是超大规模、高并发的动态网站就显得力不从心了,暴露了很多难以克服的问题,如:商城网站中对商品数据的频繁查询
、对热搜商品的排行统计
等等,虽然可以实现这些功能,但是在性能上不容乐观,NOSQL这个技术门类的出现,很好的解决了这个问题,他告诉这个世界不仅仅是sql
。
3、NOSQL的四大分类
3.1 键值(key-value)数据库
# 说明
这一类的数据库会使用到一个哈希表,这个表中有一个键和一个指针指向特定的数据。
# 特点
- 简单、易部署
- 但是在只对部分值进行更新时,key/value就显得效率低下了
# 相关产品
Redis
SSDB
...
3.2 列存储数据库
# 说明
这类数据库用来应对分布式存储的海量数据。
# 特点
键仍然存在,但是它指向了多个列,这些列是由列家族来安排的。
# 相关产品
HBase、Riak
3.3 文档型数据库
# 说明
该类型的数据模型是版本化的文档、半结构化的文档以特定的格式进行存储,比如JSON.
# 特点
以文档形式存储
# 相关产品
MongoDB
3.4 图形(graph)数据库
# 说明
使用灵活的图形模型,可以扩展到多个服务器上。
# 相关产品
InfoGrid
4、NOSQL的应用场景
- 数据模型比较简单
- 需要灵活性更强的IT系统
- 对数据库性能要求较高
- 不需要高度的数据一致性(NOSQL产品对事务的支持不是特别良好)
5、什么是Redis
Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker.
Redis是一个开源的、基于BSD协议,通过内存存储,被用作数据库、缓存和消息中间件。
6、Redis的特点
- Redis是一个高性能key/value的内存数据库;
- Redis支持丰富的数据类型(String、List、Set、ZSet、Hash);
- Redis支持持久化(内存数据持久化到硬盘中);
- Redis单进程、单线程(没有线程安全问题,但是依旧保持高效率,可用于分布式锁)
7、Redis的安装
# 下载压缩包
http://download.redis.io/releases/redis-4.0.11.tar.gz
# 安装gcc(redis是c编写的,我们要提供c的依赖)
yum install -y gcc
# 解压缩
tar -zxvf redis-4.0.11.tar.gz
# 进入解压缩后的目录,执行如下命令进行编译
make MALLOC=libc
# 编译完成后执行如下
make install PREFIX=/usr/redis/(指定安装目录)
# 进入安装目录下的bin目录,执行如下
./redis-server(启动服务端)
# 然后复制会话,执行如下
./redis-cli -h localhost -p 6379(启动客户端,-h指定服务端地址,-p指定端口)
./redis-cli -h localhost -p 6379 --raw(如果有显示中文的需要)
# 测试,在客户端执行如下
set name zs(键是name,值是value)
get name(获取键为name的值)
8、Redis的细节
8.1 Redis启动服务的细节
如果我们直接使用./redis-server启动的话使用的是这个shell脚本的默认配置。
那么如何才能使用指定的配置启动呢?我们在解压目录中找到redis.conf
配置文件,我们将它拷贝到我们的安装目录:
cp redis.conf /usr/redis/
接着,我们打开并编辑redis.conf
,找到port 6379
,修改为port 7000
vi /usr/redis/redis.conf
最后指定配置文件进行启动
./redis-server ../redis.conf
8.2 Redis中库的概念
库(database):用于存放数据的一个基本单元,库中存放着key-value键值对,每一个库都有一个自己的编号,从0开始。
默认库的个数是16,编号0-15,默认使用0号库。
如果想要切换库的话可以使用select dbid(库的编号)
,比如:select 1
8.3 Redis中清除库
flushdb
:清空当前库flushall
:清空所有库
9、Redis的相关指令
9.1 操作key相关指令
# 1、del指令
删除指定的一个或多个key,不存在的key会被忽略
del name age(返回删除的key的数量)
# 2、exists指令
判断一个或多个key是否存在,返回存在的个数
exists name age
# 3、expire指令
默认key的生存时间为永久,我们可以给它设置生存时间
expire name 10(生存时间为10秒)
# 4、keys指令
查找匹配指定模式pattern的key
keys *(查找所有key)
keys h*llo(查找首尾分别是h和llo的key)
keys h?llo(查找首尾分别是h和llo并且中间只有一个字符的key)
keys h[ae]llo(查找首尾分别是h和llo并且中间是a或者e的key)
# 5、move指令
将当前库中的key移动到其它库
move key dbid
比如:move name 1(将名字为name的key移动到1号库)
# 6、pexpire指令
和expire指令类似,用于设置生存时间,单位为毫秒
pexpire age 5000(生存时间为5000毫秒也就是5秒)
# 7、ttl指令
用于返回指定key的剩余生存时间
ttl key(如果生存时间为永久返回-1,如果key不存在返回-2,其它的则返回剩余秒数)
还有一个pttl指令则是返回剩余的毫秒数
# 8、randomkey指令
随机返回当前数据库中的某个key,如果数据库为空,则返回nil
# 9、rename指令
给库中存储的key重新命名
rename key newkey
# 10、type指令
获取key所存储的值的类型
type key
10、Redis的数据类型
10.1 String数据类型
# 设置新的key/value
set key value => set name zs
# 获取指定key存储的value
get key => get name
# 同时设置多个key/value
mset key value ... key value => mset name zs age 23 addr jx
# 同时获取多个key对应的值
mget name age addr
# 获取原始key的值,并设置新值
getset name ls
# 获取key所对应的value的长度
strlen key => strlen name
# 向指定key所对应的值追加内容,返回追加后value的长度
append key value => append name isgood
# 截取value的内容
getrange key start end => getrange name 3 -1(-1表示末尾)
# 在设置key/value时设置生存时间
setex key seconds value => setex name 10 zs(ex是expire的缩写)
还有一个psetex指令,功能类似,单位是毫秒
# key不存在则执行添加操作,存在不进行操作
setnx key value => setnx name zs
还有一个msetnx指令功能类似,不过它是进行批量添加的,并且它是原子性的,只要其中有一个key已存在,则不会进行添加操作
msetnx name zs age 23 addr jx
# 给数值类型的value进行减法操作
decr key => decr age(每次减1)
decrby key step => decrby age 10(每次减10)
# 给数值类型的value进行加法操作
incr key => incr age(每次加1)
incrby key step => incrby age 10(每次加10)
incrbyfloat key step => incrbyfloat age 0.123(与浮点数进行加法运算)
10.2 List数据类型
特点:元素有序、可以重复
10.2.1 List数据类型的内存模型
10.2.1 List相关指令
# 在列表的左边添加元素(如果列表存在)或者创建一个列表并从左往右添加元素(列表不存在)
lpush namelist zs ls ww
还有一个lpushx指令,功能和lpush一样,但是前提是key(列表)必须存在
# 在列表的右边添加元素(如果列表存在)或者创建一个列表并从右往左添加元素(列表不存在)
rpush namelist zs ls ww
还有一个rpushx指令,功能和rpush一样,但是前提是key(列表)必须存在
# 获取列表指定范围内的元素
lrange key start end => lrange namelist 0 -1(-1表示末尾)
# 移除并返回列表左边第一个元素
lpop key => lpop namelist
# 移除并返回列表右边第一个元素
rpop key => rpop namelist
# 获取列表中元素的个数
llen key => llen namelist
# 修改指定索引的值(索引必须存在)
lset namelist 0 asx
# 获取指定索引的值
lindex namelist 0
# 删除元素
lrem key count value => lrem namelist 3 zs(删除列表中最多三个“zs”)
# 保留指定区间内的元素
ltrim key start end => ltrim namelist 0 1
# 插入元素
linsert key before|after value1 value2 => linsert namelist before zs ww(在从左边数第一个“zs”前插入“ww”)
10.3 Set数据类型
10.3.1 特点
元素无序、不可重复
10.3.2 内存模型
10.3.3 相关命令
# 向set中添加元素(set存在)或者创建并向set集合中添加元素(set不存在)
sadd nameset zs ww zl ls zs
# 查询集合中所有的元素(无序)
smembers nameset
# 获取set集合的元素个数
scard nameset
# 随机移除一个(默认)或者多个元素并返回
spop nameset(移除一个)
spop nameset 2(移除两个)
# 将元素从一个集合移动到另一个集合
smove source(源set) destination(目标set) value(元素) => smove nameset nameset2 zs
# 移除指定的一个或者多个元素
srem key value1 value2 ... => srem nameset zs ww
# 判断元素是否存在
sismember key value => sismember nameset zs
# 获取一个(默认)或者多个元素
srandmember key => srandmember nameset(随机返回一个)
srandmember key count => srandmember nameset 2(随机返回两个)
# 返回集合中不同于指定的其它集合内元素的元素(简而言之,返回的不能是其它集合有的)
sdiff key1,key2... => sdiff nameset1,nameset2
# 求交集,返回多个集合共有的元素
sinter key1,key2... => sinter nameset1,nameset2
# 求并集,返回多个集合的所有元素(去除重复)
sunion key1,key2... => sunion nameset1,nameset2
10.4 ZSet数据类型
10.4.1 特点
可排序的、不可重复的set集合
10.4.2 内存模型
10.4.3 常用命令
# 向zset中添加元素(zset存在)或者创建并向zset中添加元素
zadd key score1(分数) value1 score2 value2... => zadd namezset 10 zs 20 ww 30 ls
# 获取元素个数
zcard namezset
# 返回一个范围内的元素
zrange key start end [withscores] => zrange namezset 0 -1 withscores(按照分数升序显示,并且会显示分数)
zrevrange key start end [withscores] => zrevrange namezset 0 -1 withscores
# 返回指定分数范围内的元素
zrangebyscore key startscore endscore [withscores] => zrangebyscore namezset 10 20(返回10-20分之间的元素)
# 返回某个元素的排名
zrank key value => zrank namezset ls(按分数升序的排名)
zrevrank key value => zrevrank namezset ls(按分数倒序的排名)
# 返回指定元素的分数
zscore key value => zscore namezset ls
# 移除某个元素
zrem key value => zrem namezset ww
# 给某个元素加分
zincrby key increment value => zincrby namezset 1 ls(给ls加1分)
10.5 Hash数据类型
10.5.1 特点
value是一个map结构(key-value),key是无序的。
10.5.2 内存模型
10.5.3 相关命令
# 向hash中存入一个key/value(hash存在,不存在则是新建)
hset key(bigkey) field(smallkey) value => hset personhash name zs(personhash是整个hash的key,name是其中一对键值对的key)
# 根据key获取value
hget bigkey smallkey => hget personhash name
# 获取所有的键值对
hgetall key => hgetall personhash
# 删除一个键值对
hdel bigkey smallkey => hdel personhash name
# 判断是否存在某个键值对
hexists bigkey smallkey => hexists personhash name
# 获得所有的键
hkeys key => hkeys personhash
# 获得所有的值
hvals key => hvals personhash
# 同时设置多个key/value
hmset personhash name zs age 23 addr jx
# 同时获取多个key/value
hmget personhash name age addr
# 设置一个不存在的key的值
hsetnx bigkey smallkey value => hsetnx personhash bir 2020-11-10(nx表示not exist)
# 加法操作
hincrby personhash age 1
# 浮点加法
hincrbyfloat personhash age 1.22
10.6 补充:redis可视化工具RDM(redis-desktop-manager)
- RDM的安装和使用
- 注意:在连接时如果无法成功连接,则可能是因为没有开启redis的远程连接。我们需要修改
redis.conf
文件,找到bind 127.0.0.1
,将它修改为bind 0.0.0.0
允许任意客户端连接。
11、持久化机制
Redis官方为我们提供了两种持久化方法将数据从内存保存到硬盘。
- 快照(Snapshot):保存redis某一时刻的数据状态。
- AOF(Append Only File):只追加日志文件,就是将所有的写命令记录到日志文件中。
11.1 快照(snapshot)
11.1.1 特点
可以将某一时刻的数据保存到磁盘中,这是redis默认开启的持久化方式,生成的文件以.rdb
结尾(默认生成的文件在bin目录下,叫做dump.rdb),所以也叫作RDB方式,redis会在启动时自动加载快照内保存的数据。
11.1.2 快照的生成方式
- 客户端:bgsave指令和save指令
- 服务端配置自动触发
# 客户端方式之bgsave
当在客户端使用bgsave指令时,redis会使用fork来创建一个子进程,然后子进程负责将快照写入到磁盘中,而父进程则继续处理请求。
# 客户端方式之save
当使用save指令时,将由主进程来负责生成快照以及将快照写入到磁盘(不会创建子进程),此时服务端不会响应其它的命令,也就是说此时redis处于阻塞状态,无法对外提供服务。
# 服务端方式之满足配置自动触发
当用户在`redis.conf`配置文件中配置了save选项,当满足其要求时便会触发bgsave命令,如果配置了多个save选项,则只要有一个满足,便会触发bgsave。下面是`redis.conf`中的相关配置:
################################ SNAPSHOTTING ################################
#
# Save the DB on disk:
#
# save <seconds>(秒数) <changes>(改变次数)
#
# Will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
#
# In the example below the behaviour will be to save:
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
#
# Note: you can disable saving completely by commenting out all "save" lines.
#
# It is also possible to remove all the previously configured save
# points by adding a save directive with a single empty string argument
# like in the following example:
#
# save ""
save 900 1(900秒也就是15min内有至少一个改变则会触发bgsave)
save 300 10(300秒也就是5min至少有10次改变触发bgsave)
save 60 10000(60秒也就是1min至少有10000次改变触发bgsave)
# 服务端方式之接收客户端shutdown指令
当redis接收到shutdown(关闭服务器)指令,会执行一个save命令
11.2 AOF只追加日志文件
11.2.1 快照缺陷
我们知道,只要满足配置的要求,就会自动触发bgsave命令,但是这里会有一个很致命的缺陷,假如在保存完上一次快照之后、还未达到下一次保存快照的要求之前宕机了,那么这一段时间内的数据就会丢失,所以redis在此基础上增加了AOF。
11.2.2 特点
AOF会将我们客户端执行的所有写命令记录到日志文件中,以此来记录数据的变化,我们的redis只需要从头到尾执行一次便可以恢复原来的数据状态。
11.2.3 开启AOF持久化
redis的默认配置是没有开启AOF的,我们需要在配置文件redis.conf
进行相关配置:
# 在redis.conf中找到APPEND ONLY MODE
appendonly no => appendonly yes
appendfilename "appendonly.aof"//这个是AOF日志文件的名字,可自行进行修改
11.2.4 日志追加频率
所谓的日志追加频率指的是redis多久向日志文件中追加写命令记录,redis为我们提供了三种策略:
# always(谨慎使用)
说明:每个写命令都会被同步写入到磁盘中,会严重影响redis的速度。
解释:如果使用always选项的话,每个写命令都会被写到磁盘中,可以将数据的丢失减少到最小;但是这种策略需要频繁进行写操作,redis执行命令的速度会受到磁盘性能的影响。
# everysec(推荐)
说明:每秒执行一次同步将写命令记录到日志文件中。
解释:这种策略可以兼顾数据安全和写入性能两方面,既不会因为磁盘性能而影响到redis执行命令的速度,并且最多丢失一秒钟内的数据。
# no(不推荐)
说明:由操作系统决定何时执行同步
解释:这种策略虽然不会影响redis的性能,但是系统崩溃时,会丢失不定量的数据,万一操作系统一年之后才决定执行同步呢?或者说下一次会一次性写入几个G的写命令到日志文件?
# 修改日志同步频率
在redis.conf中找到appendfsync everysec进行修改即可
11.2.5 AOF文件的重写
1.AOF带来的问题
因为每一条写命令都会被记录到日志文件中,长此以往,日志文件的体积将会越来越大。如果我们执行了100条set name 24,那么日志文件就会记录100条,其实恢复数据状态只需要一条就够了,为了压缩aof持久化文件的体积,redis为我们提供了AOF重写机制。
2.AOF重写
用于在某种程度上减小日志文件的体积
3.重写触发的方式
# 客户端方式触发重写
在客户端使用BGREWRITEAOF指令
# 服务器配置方式触发重写
我们可以在redis.conf中找到这两个东西:
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
这两个表示当日志文件的体积大于64mb,并且体积是原来体积的一倍(100%)时触发重写。
64->20->40->27->54....
4.重写原理
重写aof文件并没有读取旧的aof文件,而是将整个内存中的数据库的内容用命令的方式重写了一个新的aof文件,替换原有的aof文件,这点和快照相类似。
# 重写流程
1. redis使用fork产生子进程,子进程会将当前数据库的数据状态以命令的方式保存到临时文件中;
2. 父进程会继续处理客户端请求,将写命令记录到旧的临时文件中,并且缓存这些写命令,这样就能保证即使子进程重写失败,也能恢复到正常的数据状态;
3. 等到子进程重写完毕,就会向父进程发出信号,然后父进程就会把缓存的写命令也写入到临时文件中;
4. 最后父进程就可以使用临时文件去替换旧的aof文件了。
12、java操作redis
12.1 简单使用
首先需要引入依赖:
compile group: 'redis.clients', name: 'jedis', version: '2.9.0'
接着编写测试代码
public static void main(String[] args) {
//创建一个Jedis对象(参数是ip和端口号)
Jedis jedis = new Jedis("xxx",6379);
//选择库,默认是0号库
jedis.select(0);
//获取所有的key
Set<String> keys = jedis.keys("*");
keys.forEach(key-> System.out.println(key));
//清空库
jedis.flushDB();
//jedis.flushAll();
//释放资源
jedis.close();
}
12.2 相关操作
public class RedisTest01 {
private static Jedis jedis;
@Before
public void before() {
jedis = new Jedis("192.168.35.128",6379);
jedis.select(0);
}
//测试key相关
@Test
public void testKey() {
//获取所有的key
Set<String> keys = jedis.keys("*");
keys.forEach(key-> System.out.println(key));
//判断一个或多个key是否存在
System.out.println(jedis.exists("name", "age"));//2 存在的个数
//删除一个或者多个key
System.out.println(jedis.del("addr"));//1
//设置超时时间
jedis.expire("name",100);
//获取一个随机的key
System.out.println(jedis.randomKey());//name
}
//测试String类型
@Test
public void testString() {
//set
jedis.set("name","zs");
//get
System.out.println(jedis.get("name"));
//mset:设置多个
jedis.mset("age","23","addr","jx");
//mget:获取多个
List<String> list = jedis.mget("age", "addr");
for (String s : list) {
System.out.println(s);
}
//getSet:获取原值并设置
System.out.println(jedis.getSet("name", "ls"));
}
@After
public void after() {
jedis.close();
}
}
13、SpringBoot整合Redis
13.1 介绍
Spring Boot Data Redis为我们提供了RedisTemplate
和StringRedisTemplate
,StringRedisTemplate
是RedisTemplate
的子类,方法大致相同,区别在于,RedisTemplate
的两个泛型都是Object,意味着key和value都可以是对象,而StringRedisTemplate
的两个泛型都是String,意味着key和value只能是字符串。
- 注意点:RedisTemplate默认是将对象序列化到redis中,所以需要实现序列化接口
13.2 环境搭建
创建一个SpringBoot项目并引入相关依赖
compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis'
在配置文件中编写redis相关
# 配置redis
spring.redis.host=xxx.xxx.xxx.xxx
spring.redis.port=6379
spring.redis.database=0
13.3 测试StringRedisTemplate
@SpringBootTest(classes = Redisdemo02Application.class)
@RunWith(SpringRunner.class)
public class StringRedisTemplateTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void testString() {
stringRedisTemplate.opsForValue().set("name","zs");
stringRedisTemplate.opsForValue().set("age","23",100, TimeUnit.SECONDS);//设置超时时间为100秒
stringRedisTemplate.opsForValue().append("name"," is stupid");//追加内容
stringRedisTemplate.opsForValue().decrement("age",10);//减10
stringRedisTemplate.opsForValue().getAndSet("name","ls is smart");//获取原值并设置
}
@Test
public void testList() {
//stringRedisTemplate.opsForList().leftPush("list","zs");//左边添加
//stringRedisTemplate.opsForList().rightPush("list","ls");//右边添加
List<String> list = stringRedisTemplate.opsForList().range("list", 0, -1);//获取所有的value
list.forEach(s-> System.out.println(s));
String s1 = stringRedisTemplate.opsForList().index("list", 2);//获取指定索引处的元素
System.out.println(s1);
String s2 = stringRedisTemplate.opsForList().leftPop("list");//移除左边第一个元素
System.out.println(s2);
String s3 = stringRedisTemplate.opsForList().rightPop("list");//移除右边第一个元素
System.out.println(s3);
stringRedisTemplate.opsForList().remove("list",3,"zs");//最多删除列表中三个zs
System.out.println(stringRedisTemplate.opsForList().size("list"));//获取列表长度
}
}
13.4 RedisTemplate测试
RedisTemplate
是StringRedisTemplate
的父类,他存储的key和value都是Object,并且在存储时会对key和value进行序列化操作,所以key和value所在类必须实现序列化接口(Serializable):
@SpringBootTest(classes = Redisdemo02Application.class)
@RunWith(SpringRunner.class)
public class RedisTemplateTest {
@Autowired
private RedisTemplate redisTemplate;
//测试String
@Test
public void testString() {
redisTemplate.opsForValue().set("name","zs",100, TimeUnit.SECONDS);//\xac\xed\x00\x05t\x00\x04name key经过了序列化
System.out.println(redisTemplate.opsForValue().get("name"));
}
//测试Object
@Test
public void testObject() {
User user = new User();
user.setName("zs");
user.setAge(23);
redisTemplate.opsForValue().set("user",user);//\xac\xed\x00\x05t\x00\x04user
User u = (User) redisTemplate.opsForValue().get("user");
System.out.println(u.getName());//zs
}
//序列化方案
@Test
public void testSerializer() {
//RedisTemplate默认使用JDK序列化方案,我们一般会将key的序列化方案设置为String类型序列化,这样也方便在终端查看
redisTemplate.opsForHash().put("map","name","zs");//hash类型
System.out.println(redisTemplate.getKeySerializer());//获取大key的序列化方案 JdkSerializationRedisSerializer
System.out.println(redisTemplate.getHashKeySerializer());//获取小key的序列化方案 JdkSerializationRedisSerializer
//修改序列化方案 String类型序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
}
}
13.5 Bound Api
为什么要有Bound Api(绑定api)
,我们可以发现,每次我们使用RedisTemplate
或者StringRedisTemplate
进行操作时都需要带上key
,假如我们需要连续多次对同一个key
进行操作的话,显然很不友好,所以Spring Data为我们提供了绑定api,它可以让我们去绑定一个key,并且在后续操作时不需要带上key。
//bound api
@Test
public void testBoundApi() {
BoundHashOperations hashOperations = redisTemplate.boundHashOps("user");
hashOperations.put("name","zs");//不需要带上大key user
hashOperations.put("age",23);
System.out.println(hashOperations.get("name"));//zs
}