目录
NoSQL
含义:NoSQL(not only sql),意为不仅仅是sql,泛指非关系型数据库,其内部的数据存储在内存中;nosql不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大增加了数据库的扩展能力。
NoSQL特点
- 因为其数据存储在内存中,所以具有远超sql的性能
- 不遵循sql标准
- 不支持ACID
- 数据结构相对复杂,复杂查询方面稍欠
常见的NoSQL数据库
redis
- 数据都存在内存当中,支持持久化,主要用作备份恢复
- 除了支持key-value模式,还支持多种数据结构的储存,比如list、set、hash、zset等
- 一般是作为缓存数据库辅助持久化数据库的
MongoDB
- 高性能、开源、模式自由的文档型数据库
- 数据都存在内存中,若内存不足,把不常用的数据保存到硬盘
- 虽然是key-value模式,但是对于value(尤其是json)提供了丰富的查询功能
- 支持二进制数据及大型对象的存储
- 可以根据数据的特点替代RDBMS,成为独立的数据库;或者配合RDBMS,存储特定的数据
行式存储数据库
列式存储数据库
redis简介
- redis支持的key-value模式其中key的数据类型为string,value的数据类型可以为string(字符串)、list(链表)、set(集合)、hash(哈希类型)、zset(有序集合)。
- 这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的;在此基础上,redis也支持各种不同方式的排序
- redis的数据都是缓存在内存中,其会周期性的把更新的数据写入磁盘或者把修改的操作写入追加的记录文件中并在此基础上实现主从同步
redis官网:https://redis.io/
安装redis
- 检查linux系统内有没有gcc环境:gcc --version
- 没有gcc环境则安装:yum install gcc(此过程需要虚拟机联网,详见网络基础帖子)
- 将redis-7.0.4.tar.gz放入linux文件系统中
- 解压文件:tar -zxvf redis-7.0.4.tar.gz
- 进入项目目录:cd redis-7.0.4
- 使用make命令将其编译成C文件
- 编译好后用make install完成安装
进入安装目录:cd /usr/local/bin
- redis-benchmark:性能测试工具,可以在自己的本子上运行,看看自己本子性能如何
- redis-check-aof:修复有问题的AOF文件
- redis-check-rdb:修复有问题的dump.rdb文件
- redis-sentinel:redis集群中使用
- redis-server:Redis服务器启动命令
- redis-cli:redis客户端,操作入口
redis启动服务
前台启动
敲击命令:redis-server
提取信息
- Running in standalone mode:redis单机模式启动
- Port: 6379:启动端口号为6379
- PID: 33332:进程ID为33332
注意:前台启动后此窗口便不能再进行其他操作了,关了服务就没了
后台启动
理解:该启动方式我们看不到上面的界面,并且,我们关闭窗口后那么他还可以继续使用
启动方法
- 进入redis解压后的目录:cd redis-7.0.4
- 将redis.conf文件复制一份:cp redis.conf /root/redis-conf(复制到哪个目录都可以)
- 将复制后的文件后台启动设置daemonize no改为yes后保存并退出(vi redis.conf)
- 敲redis-server /root/redis-conf/redis.conf命令来启动redis
- ps查看redis进程:ps -ef | grep redis
通过客户端连接redis
敲击:redis-cli
测试redis的连通状态:敲击ping后回应pong那么表示已经连通
redis的关闭
- shutdown:该方式安全,关闭后会将数据持久化
- exit:直接退出
redis数据库相关知识
操作数据库命令集
- Memcache使用的技术:多线程+锁
- redis使用的技术:单线程+多路IO复用
redis的多路复用
- 首先,redis是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以IO操作在一般情况下往往不能直接返回,这就会导致某一文件的IO阻塞导致整个进程无法对其他的客户提供服务,而IO多路复用就是为了解决这个问题而出现的(多个用户将请求交给中介,redis对中介手机的任务采取epoll策略执行)
- select、poll、epoll都是多路复用的机制。IO多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪,就能够通知程序进行相应的操作(基于一种通知机制)
- redis的io模型主要是基于epoll实现的,不过他也提供了select、kqueue的实现,但是默认就为epoll
多路复用的机制
- select:基于一种长轮询,他会不断地遍历每个任务,确认是否完成;完成后提交,未完成后忽视继续遍历其他任务;这种机制管理的任务最多为1024个
- poll:和上面的select机制一样,唯一不同的是它管理的任务不限
- epoll:基于通知的机制,一次遍历完所有任务,完成则提交,未完成则通知任务在完成后需要执行下个指令
redis的key基本操作
- 为具体的key赋值为value:set key value
- 取出对应key的value值:get key
- 删除指定的key数据:del key(删除成功则返回1,否则返回0;可以清空多个)
- 删除指定的key数据:unlink key(删除成功则返回1,否则返回0)
- 注意:del直接删除,而unlink的删除他仅将其从keys内删除,真正的删除会在后续异步执行
- 查看该数据库中的所有key:keys *
- 查看key对应的value具体数据类型:type key
- 查看key是否存在:exists key(存在则返回1,否则返回0)
- 为key设置过期时间为10秒钟:expire key 10(成功则返回1,否则返回0)
- 为key设置过期时间为10000ms:pexpire key 10000(成功则返回1,否则返回0)
- 查看key的剩余过期时间:ttl key(-1表示永不过期,-2表示已过期)
- 取消key的过期时长:persist key
redis数据类型
redis的key的数据类型为string;value的数据类型支持5种:String(字符串)、List(集合)、Set(集合)、Hash(哈希)、Zset(有序集合)
String类型
- String是redis的最基本的数据类型,一个key对应一个value
- String类型是二进制安全的,意味着redis可以包含任何数据类型,比如jpg图片或者序列化的对象
- 一个redis中字符串的value最多可以是512M
基本操作
- 为key赋值为value:set key value(若key存在则value覆盖)
- 为key赋值为value:setnx key value(若key存在,则赋值失败)
- 取出key的值:get key
- 为key后面追加hello:append key hello(成功返回key的长度)
- 获得key对应value的长度:strlen key
- 为key值递增1:incr key(返回递增后的值)
- 为key值递减1:decr key(返回递减后的值)
- 为key值递增n:incrby key n(返回递增后的值)
- 为key值递减n:decrby key n(返回递减后的值)
- 注意:对于递增/递减操作,那么要递增的值必须为数字类型
- 设置k1值为v1,k2值为v2:mset k1 v1 k2 v2
- 设置k1值为v1,k2值为v2:msetnx k1 v1 k2 v2(里面的key有任何一个存在则都不会设置成功)
- 获取k1和k2的值:mget k1 k2
- 删除k1和k2的值:del k1 k2
- 获取value的范围内的字符:getrange k1 2 3(获取value的第3个到第4个间的字符,包前包后)
- 在k1的第2个索引处设置hello:setrange k1 2 hello(返回value的长度)
- 设置值时顺便设置10s过期时间:setex key 10 value
- 新值换旧值:getset key value(返回是之前的值)
String的数据结构
String的数据结构为简单动态字符串,是可以修改的字符串,采用预分配冗余空间的方式来减少内存的频繁分配
如图所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1m时,扩容都是加倍现有空间,若超过1m,扩容时只会多扩1m的空间,但字符串value最大空间为512m
List类型(单键多值)
- redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
- 他的底层实际上是双向链表,对两端的操作性能很高,通过索引下标的操作中间节点性能比较差
常用命令
- 从左边插入多值:lpush key v1 v2 v3
- 从右边插入多值:rpush key v1 v2 v3
- 从左边吐出一个值:lpop key
- 从右边吐出一个值:rpop key
- 按照索引从左到右获得元素:lrange key 0 -1(0到-1表示取所有值)
- 从k1右边取值插入k2左边:rpoplpush k1 k2
- 根据索引下标获得指定元素:lindex key 2(获得第二个元素)
- 获取列表的长度:llen key
- 在key中v1前面插入新值:linsert key before v1 newv1(若在后面就把before换为after)
- 从左边删除n个v1:lrem key n v1
- 将列表下标为index的值替换为value:lset key index value
- 从key集合中保留下标为1到结束的数据:ltrim key 1 -1
List的数据结构
- 首先在列表元素较少的情况下会使用一块连续的内存储存,这个结构是ziplist,也即是压缩列表他将所有的元素紧挨着一起储存,分配的是一块连续的内存
- 当数据量比较多的时候,redis将链表和ziplist组合起来成quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速插入和删除性能,又不会出现太大的空间冗余
Set集合
Set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动去重的,redis的set是string类型的无序集合,他的底层其实上就是一个value为null的hash表,所以添加、删除、查找的复杂度都是O(1)
基本操作
- 将多个值加入到key集合:sadd key v1 v2 v3
- 取出key集合中的值:smembers key
- 判断key集合中是否包含value:sismember key value(包含返回1,否则返回0)
- 返回key集合中元素的个数:scard key
- 删除key集合中的v1和v2:srem key v1 v2
- 随机从集合中吐出一个值:spop key
- 随机从集合中显示n个值:srandmember key n
- 把集合中的value值从k1集合移动到k2集合:smove k1 k2 value
- 返回两个集合中的交集元素:sinter k1 k2
- 返回两个集合中的并集元素:sunion k1 k2
- 返回k1集合中不包含k2的元素:sdiff k1 k2
set集合数据结构
set数据结构是dict字典,字典是用哈希表实现的,redis的内部也是使用hash结构,所有的value都指向同一个内部值
Hash类型
Redis hash是一个键值对集合,其是一个string类型的field和value的映射表,hash特别适用于存储对象,类似java里面的Map<String Object>
常用命令
- 将user对象进行存储:hset user name lili age 23(hmset也可以)
- 取出user的name:hget user name(只能取出一个属性)
- 获得user的多个属性:hmget user name age
- 取出user的所有数据:hgetall user
- 判断user的name属性是否存在:hexists user name(存在返回1,不存在返回0)
- 列出该user的所有field:hkeys user
- 列出user集合中的所有属性的值:hvals user
- 为user的age属性加2操作:hincrby user age 2
- 为user对象添加属性与值:hsetnx user sex man(若属性存在,则添加失败)
- 删除user的age与name属性:hdel user age name
hash数据结构
Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable
Zset有序集合
zset与普通set集合十分相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score),这个评分被用来按照从最低分到最高分的方式排序集合中的成员。集合中的成员是唯一的,但评分可以重复
注意:zset内的元素会默认根据score评分从小到大排序
常用命令
- 将一个或多个元素以及对应的score加入到有序集合key中:zadd key 100 java 200 c++ 300 php
- 返回有序集合key下标在开始到结束之间的元素:zrange key 0 -1 [withscores]
- 注意:后面添加withscore后那么就会把他们对应的评分也显示
- 取出key中评分在150-400之间的值:zrangebyscore key 150 400 [withscores]
- 从大到小显示key中score值为700-200之间的值:zrevrangebyscore key 700 200 [withscores]
- 将key中的java元素的评分增加50:zincrby key 50 java
- 删除集合下指定值的元素:zrem key php
- 统计key中评分为100-200之间值的个数:zcount key 100 200
- 获取集合中成员的个数:zcard key
- 返回该值在集合中的索引:zrank key java
zset底层使用2个数据结构
- hash,hash的作用就是关联元素value和权重score,保证元素value的唯一性,可以通过value找到相应的score值
- 跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表(空间换时间)
有序链表——查询慢,省空间
跳跃表(建索引)——查询快,费空间
redis6新数据类型
Bitmaps类型
- Bitmaps本身不是一种数据类型,实际上它就是字符串(key-value),但是它可以对字符串的位进行操作
- Bitmaps单独提供一套命令,所以在redis中使用Bitmaps和使用字符串的方法不太相同。可以把Bitmaps想象成一个以位为单位的数组,数组的每个单位只能存储0和1,数组的下标在Bitmaps中叫做偏移量
基本命令
- 设置某个偏移量的值(0/1):set key 19 1
- 理解:设置偏移量为19的地方的值为1(偏移量就理解成索引)
- 取出偏移量为1对应的值:getbit key 1(返回1下标处的值)
- 统计key偏移量在下标为[1,2]值处为1的总和:bitcount key 1 2
HyperLogLog类型
- redis HyperLogLog是用来做基数统计的算法,HyperLogLog的优点是,在输入元素的数量或体积非常非常大时,计算基数(例如求和)所需的空间总是固定的,并且很小很小空间
- 在redis里面每个HyperLogLog键只需要花费12kb内存就可以计算接近2^64个不同的基数。这和计算基数时,元素越多越耗费内存的集合形成鲜明的对比
- 由于HyperLogLog只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素
常用命令
- 向key中加入数据:pfadd key v1 v2 v3(若value相同则添加失败,返回0,否则返回1)
- 统计key中value的数量:pfcount key
- 将k1和k2的value加到k3里:pfmerge k3 k1 k2
Geospatial类型
redis 3.2中增加了对GEO类型的支持。GEO、Geographic,地理信息的缩写。该类型就是元素的二维坐标,在地图上就是经纬度。redis基于该类型提供了经纬度设置,查询,范围查询,距离查询,经纬度hash等常见操作
基本命令
- 加上海地理信息:geoadd china:city 121.47 31.23 shanghai
- 同时加上海和深圳:geoadd china:city 121.47 31.23 shanghai 114.05 22.52 shenzhen
- 获得指定地区的坐标值:geopos china:city shanghai shenzhen
- 获取两个位置之间的直线距离以km为单位:geodist chain:city shanghai shenzhen km
- 以给定的经纬度为中心,找出某一半径内的元素:georadius china:city 110 30 1000 km
- 理解:计算出东京110度北纬30度在里面方圆1000km以内的城市
注意:
- 有效的经度从-180度到180度。有效的维度从-85.05112878度到85.05112878度
- 当坐标位置超出指定范围时,该命令将会返回一个错误
redis配置文件
配置大小单位,开头定义了一些基本的度量单位,只支持byte,不支持bit;大小写不敏感
理解:include包含其他文件内容
bind=127.0.0.1:只能接收本机的访问请求,不写的情况下无限制接收任何ip地址的访问
protect-mode no:保护模式开启情况下,redis只允许接受本机的响应
port 6379:端口号默认6379
tcp-backlog 511:backlog为一个连接队列
注意:backlog队列总和=未完成三次握手的队列+已完成三次握手的队列
timeout 0:redis连接永不超时(秒为单位)
tcp-keepalive 300:每隔300s检测一次连接是否存活
daemonize yes:是否支持redis后台启动,yes表示是
pidfile /var/run/redis_6379.pid:将进程号设置到该文件中保存
loglevel notice:设置日志级别
logfile "":设置日志的输出文件路径,默认为空
database 16:redis默认有16个库
内存淘汰策略
redis的发布订阅
redis发布订阅是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息,redis客户端可以订阅任意数量的频道
理解:发送者可以发送很多频道,若订阅者订阅发布者的所有频道,那么当发布者发布时,订阅者一定能收到消息,若订阅者只订阅发布者的a频道,而发布者向b频道发送消息,那么订阅者便不能收到消息
具体操作
- 打开两个redis的客户端(redis启动方式为后台启动)
- 一个客户端订阅channel1频道:subscribe channel1
- 另一个客户端发送信息向channel1:publish channel1 hello
第二个客户端就收到了
jedis操作redis数据库
前提:配置文件中配置允许远程连接
导入依赖
<dependencies>
<!--redis客户端依赖-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.2.3</version>
</dependency>
</dependencies>
连接测试
public static void main(String[] args) {
//创建jedis的对象(远程访问redis服务)
Jedis jedis = new Jedis("192.168.126.129",6379);
//测试是否连接上redis,若成功,返回pong
String ping = jedis.ping();
System.out.println(ping);
}
注意:连接时应先关闭防火墙
关于key的基本操作
public static void main(String[] args) {
//创建jedis的对象(远程访问redis服务)
Jedis jedis = new Jedis("192.168.126.129",6379);
//jedis.auth("123456");密码
//选择1号数据库
jedis.select(1);
//添加元素
jedis.set("name", "lili");
//获得该数据库下所有的key
Set<String> keys = jedis.keys("*");
//查看keys的个数
System.out.println(keys.size());
for (String key : keys) {
System.out.println(key);
}
//判断name是否存在
System.out.println(jedis.exists("name"));
//查看name的过期时间
System.out.println(jedis.ttl("name"));
//获得name的值
System.out.println(jedis.get("name"));
//为name设置过期时间10s
jedis.expire("name", 10);
//删除name
jedis.del("name");
//设置多个key-value
jedis.mset("age","23","sex","man");
List<String> mget = jedis.mget("age", "sex");
System.out.println(mget);
jedis.close();
}
list类型简单操作
@Test
public void test1(){
//创建jedis的对象(远程访问redis服务)
Jedis jedis = new Jedis("192.168.126.129",6379);
//添加元素
jedis.lpush("k1", "v1","v2","v3");
//取出元素
List<String> k1 = jedis.lrange("k1", 0, -1);
System.out.println(k1);
//其他的一样(太多了没必要)
}
set简单操作
@Test
public void test2(){
Jedis jedis = new Jedis("192.168.126.129",6379);
jedis.sadd("people", "lili","lala","lan");
Set<String> people = jedis.smembers("people");
System.out.println(people);
}
hash简单操作
@Test
public void test3(){
Jedis jedis = new Jedis("192.168.126.129",6379);
jedis.hset("user", "name","lili");
String hget = jedis.hget("user", "name");
System.out.println(hget);
//hmset,hmget可以操作多个
}
zset的简单操作
@Test
public void test4(){
Jedis jedis = new Jedis("192.168.126.129",6379);
jedis.zadd("china", 100d, "shanghai");
Set<String> china = jedis.zrange("china", 0, -1);
System.out.println(china);
}
注意:以上太多方法,不再一一演示,基本大同小异,照猫画虎即可
连接池实现redis操作数据库
注意:依赖还是用上面的
public static void main(String[] args) {
//定义连接池配置
JedisPoolConfig config = new JedisPoolConfig();
//设置最大连接数
config.setMaxTotal(1000);
//设置最大空闲数
config.setMaxIdle(60);
//创建连接池对象
JedisPool jedisPool = new JedisPool(config,"192.168.126.129",6379);
//从池中获取连接
Jedis resource = jedisPool.getResource();
//resource.auth("root");密码
String ping = resource.ping();
System.out.println(ping);
resource.close();
jedisPool.close();
}
SpringBoot整合redis
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring集成redis所需要的连接池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
</dependencies>
配置文件内配置redis
#application.yml文件内
server:
port: 8080
spring:
redis:
host: 192.168.126.129
port: 6379
#redis的默认数据库
database: 0
timeout: 1800000
lettuce:
pool:
#连接池最大连接数:20
max-active: 20
#最大等待时间
max-wait: -1
#最大空闲连接数
max-idle: 5
#最小空闲连接数
min-idle: 0
创建redis配置类
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
基于string类型的操作
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
//基于字符串操作对象
ValueOperations value = redisTemplate.opsForValue();
//设置值
value.set("name", "lili");
//从redis内获取值
String name = (String) value.get("name");
System.out.println(name);
}
基于hash类型操作
@Autowired
private RedisTemplate redisTemplate;
@Test
public void test1(){
//基于hash操作对象
HashOperations hash = redisTemplate.opsForHash();
//设置值
hash.put("user", "name", "lili");
//取值
Map user = hash.entries("user");
System.out.println(user);
}
基于list的操作
@Autowired
private RedisTemplate redisTemplate;
@Test
public void test2(){
//基于list操作
ListOperations list = redisTemplate.opsForList();
list.leftPush("k1", "v1");
list.leftPushAll("k1", "k2","k3");
List k1 = list.range("k1", 0, -1);
System.out.println(k1);
}
基于set的操作
@Autowired
private RedisTemplate redisTemplate;
@Test
public void test3(){
SetOperations set = redisTemplate.opsForSet();
set.add("setKey1", "A","B","C","C");
Object members=set.members("setKey1");
System.out.println("setKeys="+members);
}
事务和锁机制
含义:redis事务是一个单独的隔离操作:事务中的所有命令都会被序列化,按照顺序的执行。事务在执行过程中,不会被其他客户端发送来的命令请求所打断
redis事务的主要作用:串联多个命令,防止别的命令插队
redis与mysql中锁机制
- mysql对事务的处理采用悲观锁机制;就是当你执行了此事务,正在修改某条属性,并且你没有提交事务,那么其他人再修改事务里面的属性,则会被直接阻塞
- redis对事务的处理采用乐观锁的方式;就是当你开启了事务对一些属性进行修改并且你没有提交事务,其他人再修改事务中的某个属性则会修改成功,但是由于你开启事务后你所修改的属性会添加到一个队列(先进先出)里,由于别人修改了属性,最终会导致你的整个事务提交失败(你所修改的属性必须被watch,eg:watch key1 key2……)
基本指令
- watch key1 key2:监视key值,若监视得key值发生变化,则提交事务会失败
- unwatch:取消所有key的监视
- multi:开启事务
- exec:提交事务
- discard:取消事务
理解
从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,redis会将之前的命令队列中的命令依次执行
注意:
- 组队的过程中可以通过discard来放弃组队
- 若在发送exec命令时客户端断线,则redis会清空事务队列,事务中的所有命令均不执行
- 当一个事务中出现了错误指令时(组队时就失败了),然后再提交事务,则该事务无效
- 若一个事务中组队成功,但执行时错误,除了该错误的命令,其他的命令均成功(无论前后)
实践过程
jedis中的经典案例
注意:使用时先引入redis客户端依赖
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.126.129",6379);
jedis.set("tony", "300");
//监视tony
jedis.watch("tony");
//开启事务
Transaction multi = jedis.multi();
try {
multi.incrBy("tony", 100);
multi.exec();
}catch (Exception ex){
//若抛异常则取消事务
multi.discard();
}finally {
jedis.close();
}
}
注意:只要在key值被监视和提交事务之间的时间段内name值发生变化,那么提交后的返回值为nil
总结
在并发执行时,为了更好的保证事务的4个特性,通常会对事务加锁,redis为了性能采用了乐观锁的方式进行事务控制,它使用watch命令监视给定的key,当exec时,若监视的key从调用watch后发生了变化,则整个事务会失败。也可以调用watch多次监视多个key。
注意:
- watch监视属性应在开启事务之前
- watch的key是对整个连接有效的,若连接断开,监视和事务都会被自动取消,当然,exec、discard、unwatch命令都会清除连接中的所有监视
redis事务的三个特性
- 单独的隔离操作:事务中的所有命令都会被序列化,按顺序的执行;事务在执行的过程中,不会被其他客户端发来的请求所打断
- 没有隔离级别的概念:队列中的命令没有提交之前都不会被实际执行
- 不保证原子性:事务中若有一条命令执行失败,其后的命令仍然会被执行,没有回滚
redis持久化
持久化:把内存中的数据写到磁盘中
持久化原因:redis为内存数据库,内存中的东西在断电时可能会丢失
redis中的两种持久化方式
- RDB:快照方式,也是redis的默认开启的方式(redis启动时就开启了),他类似ctrl+c与ctrl+v,通过快照技术可以将数据恢复到某一时间点上的数据
- AOF:日志记录方式,记录你执行过什么指令,一旦数据出现问题,将之前的指令在重新执行一下
RDB
含义:在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的快照,它恢复时是将快照直接读到内存中
备份的执行过程
redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束,再用这个临时文件替换掉之前的文件。整个过程中,主进程不进行任意的io操作,这就确保了极高的性能;若需要进行大规模数据恢复,且对于数据恢复的完整性不是非常敏感,那么RDB的方式要比AOF的方式更加高效。RDB的唯一缺点就是最后一次持久化的数据可能丢失
理解
redis持久化后会生成一个文件,文件名字默认为dump.rdb,但redis并不是直接就把数据写到dump.rdb文件中,而是先创建一个子进程(进程名为fork),然后,当持久化时先在里面创建一个临时的文件,先将数据同步到临时区域中,当数据同步完成之后便会将里面的内容覆盖到dump.rdb文件中(目的:防止数据不完整)——(写时复制技术)
dump.rdb配置文件
dbfilename dump.rdb:持久化文件名为dump.rdb
dir ./:在redis的启动目录(/usr/local/bin)中生成这个rdb文件
stop-writes-on-bgsave-error yes:当redis无法写入磁盘的话,那么直接关闭redis的写操作
rdbcompression yes:持久化文件时,持久化文件进行压缩操作
rdbchecksum yes:检查数据的完整性(是否损坏)
save 60 1000:每隔60s至少有1000个key发生变化,那么就生成一个dump.rdb快照
调用命令手动生成快照
- save:save只管保存,其他不管,全部阻塞(同步保存)
- bgsave:保存过程在后台运行,其不阻碍你对redis的后续操作(异步保存——redis自动fork一个子进程来完成)
rdb持久化优势
- 适合大规模的数据恢复
- 对数据完整性要求不高时适合使用
- 节省磁盘空间
- 恢复速度快
rdb持久化劣势
- fork的时候,内存中的数据被克隆了一份,浪费空间
- redis在fork时用到写时拷贝技术,若数据庞大则消耗了性能
- 一段时间备份一次,所以redis意外down掉的话,就会丢失最后一次快照的所有修改
AOF
含义:通过记录写操作日志的方式记录redis数据的一种持久化机制,此机制默认是关闭的,此方式将每一个收到的命令都通过wiite函数增加到日志文件的最后(读操作不记录),redis重启会读取该文件,重新构建数据
AOF配置文件
appendonly yes:AOF文件开启(默认为no)
appendfilename "appendonly.aof":AOF文件名为appendonly.aof
appendfsync everysec:每秒记录日志一次,若宕机,本秒内的数据可能丢失
no-appendfsync-on-rewrite yes:在重写期间,对新写入的不进行持久化,暂存在内存中,重写完再持久化
auto-aof-rewrite-percentage 100:文件达到100%时重写启动
auto-aof-rewrite-min-size 64mb:AOF日志文件启动重写的文件最小值为64mb
注意:
- AOF文件默认是关闭的
- 若AOF和RDB同时开启,那么redis默认会取AOF的数据
AOF文件的修复
- 开启AOF配置
- 遇到文件损坏敲命令:redis-check-aof --fix appendonly.aof
- 重启redis
Rewrite压缩
AOF采用文件的追加方式,文件会越来越大为避免此种情况,新增了重写机制,当AOF文件的大小超过了所设定的阈值,redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令bgrewriteaof
重写原理
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后在rename),redis4.0版本后的重写,实质上就是把rdb的快照以二进制的形式附在新的aof的头部,作为已有的历史数据,替换掉原来的流水账操作
重写流程
主进程fork出子进程进行重写,子进程遍历redis内存中的数据到临时文件,子进程写完该临时文件后向主进程发信号,由主进程把该临时文件中的数据写入到新的aof文件中,进而用新aof文件替换就aof文件
AOF持久化流程
- 客户端的请求写命令会被append追加到AOF缓冲区中
- AOF缓冲区根据AOF持久化策略(aways、everysec、no)将操作同步到磁盘的AOF文件中
- AOF文件大小超过重写策略或手动重写时,会对AOF文件进行重写,压缩AOF文件容量
- redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的
AOF优势
- 备份机制更稳健丢失数据概率更低
- 可读的日志文本,通过操作AOF稳健,可以处理误操作
AOF劣势
- 比起RDB占用更多的磁盘空间
- 恢复备份速率更慢
- 每次读写同步的话,有一定的性能压力
- 存在个别bug
注意:官方推荐用dump.rdb作为第一步的数据恢复,然后用appendonly.aof作为第二步的数据增量
主从复制
含义:主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主(也可以读),slave以读为主(只能读)
主从复制的优势
- 读写分离,减缓主服务器压力
- 容灾的快速恢复,一个节点宕机,其他节点仍然可用
创建一主两从
关闭AOF:appendonly no(不关也可以,AOF文件改个名,别都一样)
复制redis.conf文件到myredis(文件夹名随意起)文件夹中起名也叫redis.conf
创建3个配置文件:redis6379.conf、redis6380.conf、redis6381.conf
在3个配置文件中按照格式写入内容
#从外面引入redis.conf配置文件
include /root/myredis/redis.conf
#将该文件的进程号设置到该路径保存
pidfile /var/run/redis_6379.pid
#设置端口号
port 6379
#设置持久化rdb文件名
dbfilename dump6379.rdb
启动三台redis服务器
通过端口号连接redis:redis-cli -p 6379
启动三台redis,查看三台主机的运行情况:info replication
由此观之三台机器全是主机
配置主从(我要6379为主节点)
除了6379端口以外的服务执行命令:slaveof 主节点ip地址 主节点端口号(slaveof 127.0.0.1 6379)
由此观之成功(主机可以读写,从机只能读)
注意:
- 当从服务器挂掉之后,那么从服务器就会单独变成主服务器,不影响之前的主从结构
- 当从服务器挂掉之后再重新启动变为从服务器时,那么他的数据便会从头开始复制(之前主服务器有什么,那么现在从服务器就有什么)
- 若主服务器挂掉了,那么从服务器还是从服务器,主服务器重启之后还是主服务器
主从复制的原理
- 当从服务器连接上主服务器之后,从服务器向主服务器发送进行同步的消息
- 主服务器接到了从服务器发送过来的消息,那么会把主服务器的内容进行持久化(rdb)把rdb文件发送给从服务器,从服务器拿到rdb文件进行读取(全量复制)
- 每次主服务器进行写操作后,就会和从服务器进行数据的同步(整个过程是异步的,主从同步是数据的同步)(增量复制)
薪火相传:一个主服务器,一个次主服务器,一个从服务器;从服务器只能从次主服务器中同步数据,次主服务器只能从主服务器中同步数据(白话:一个从服务器再挂一个从服务器)
反客为主:当一个master宕机后,后面的slave可以立即升为master,其后面的salve不用做任何修改(薪火相传的前提下,次主服务器升主)
(反客为主)主机挂掉后敲击命令:slaveof no one
哨兵模式(观察者模式)
含义:反客为主自动版,能够后台监控主机是否故障,若故障了则根据投票数自动将从库转为主库
哨兵模式的实践
自定义/myredis(特定目录下就可以)目录下新建sentinel.conf文件(名字必须为这个)
sentinel monitor mymaster 127.0.0.1 6379 1
#注意:mymaster为给监控对象起的名称,1为至少有多少个哨兵同意迁移的数量
启动哨兵:redis-sentinel sentinel.conf
注意:若哨兵选出了新主服务器,那么原主机若重启则会为新主机的从服务器
进阶配置
sentinel monitor mymaster 127.0.0.1 6380 1
#后台运行
daemonize yes
#运行日志所在文件
logfile "/root/myredis/sentinel_log.log"
#主节点失效30秒后被认为失效
sentinel down-after-milliseconds mymaster 30000
启动哨兵:redis-sentinel sentinel.conf
主服务器选举规则
- 选择优先级靠前的:redis.conf内:replica-priority 100,值越小优先级越高
- 选择偏移量最大的:偏移量最大是指获得原主机数据最全的
- 选择runid最小的从服务:每个redis实例启动后都会随机生成一个40位的runid
redis集群
- redis集群实现了对redis的水平扩容,即启动n个redis节点,将整个数据库分布存储在这n个节点中,每个节点存储总数据的1/n
- redis集群通过分区来提供一定程度的可用性,即使集群中有一部分节点失效或者无法进行通信,集群也可以继续处理命令请求
注意:无中心化集群(任何一台服务器都可以作为集群的入口)
搭建三主三从
制作6个实例,6379、6380、6381、6389、6390、6391(文件名随意:redis端口号.conf)
关闭AOF:appendonly no(不关也可以,AOF文件改个名,别都一样)
复制redis.conf文件到myredis(文件夹名随意起)文件夹中起名也叫redis.conf
在6个配置文件中按格式写如下内容
include /root/myredis/redis.conf
pidfile "/var/run/redis_6379.pid"
port 6379
dbfilename "dump6379.rdb"
#开启集群
cluster-enabled yes
#集群配置文件名字
cluster-config-file node-6379.conf
#设置节点失联时间,超过该时间集群节点会自动进行主从切换
cluster-node-timeout 15000
使用查找替换修改另外5个文件(:%s/6379/6380)
理解:将linux文件内的6379转换为6380
启动6个redis服务:(redis-server redis6379.conf)
将6个节点合成一个集群
注意:组合之前,应确保所有的redis实例启动后,nodes-xxxx.conf文件都能正常生成
合体:
进入之前redis的解压目录的src目录:/root/redis-7.0.4/src/在该目录里执行命令:
redis-cli --cluster create --cluster-replicas 1 192.168.126.129:6379 192.168.126.129:6380 192.168.126.129:6381 192.168.126.129:6389 192.168.126.129:6390 192.168.126.129:6391
注意:
- create --cluster-replicas 1表示以最简单的方式创建集群replicas表示以集群方式,1表示最简单的方式(一个主机一个从机为1组,共3组)
- 这里的ip不能写127.0.0.1,只能写实际ip地址
由此观之:因为我的create --cluster-replicas 1配置导致自动实现3主三从的最简单方式
注意:若集群中的主节点挂掉,那么从节点便会升为主节点,若主节点再启动,那么他会变为从机
之前我已经说过,该集群为无中心化集群(任何一个节点都可以作为集群的入口,并且他们之间可以互相访问)
集群连接:redis-cli -c -p 6379
注意:-c表示集群方式连接,集群方式连接用任何一个节点都可以
在节点内查看集群中节点的信息:cluster nodes
注意:最后面有插槽信息,请仔细看(一主一从分配一段插槽范围)
在节点内查看集群基本的信息:cluster info
16384 slots covered含义
- 一个集群中共有16384个插槽(0-16383),数据库中的每个键都属于这16384个插槽中的其中一个
- 根据你的key计算插槽,再向插槽中加入你的数据
- 集群中的每个节点负责处理一部分插槽(每个节点只能看到自己插槽内的值)。
根据key来计算插槽,那么一次添加多个key结果
- 结果报错,若想加入多个key到插槽中,那么就需要将该key分成一组,然后根据组来计算属于哪个插槽
- 将name和age放入user组存入:mset name{user} lili age{user} 23
插槽基本命令
查看key的插槽值:cluster keyslot key
查看5474插槽内有多少key:cluster countkeysinslot 5474
在5474插槽中返回10个slot的键:cluster getkeysinslot 5474 10
redis.conf内的配置
- cluster-require-full-coverage yes:默认为yes,若某一段插槽的主从都挂掉,那么整个集群都挂掉
- cluster-require-full-coverage no:若某一段插槽的主从都挂掉,那么只是该插槽挂掉不能使用
集群的jedis开发
前提:添加redis客户端依赖
public static void main(String[] args) {
//创建对象
//集群入口节点——当然,也可以创建一个set集合放入多个地址与端口号的hostAndPost,但redis集群是无中心化的,所以一个就够了
//用任何一个节点都能连上我们的集群
HostAndPort hostAndPort = new HostAndPort("192.168.126.129", 6379);
JedisCluster jedisCluster = new JedisCluster(hostAndPort);
//进行操作
jedisCluster.set("k1", "v1");
String k1 = jedisCluster.get("k1");
System.out.println(k1);
jedisCluster.close();
}