目录
1、NoSQL数据库
NoSQL数据库概述
NoSQL(Not Only SQL),意思是“不仅仅是SQL”,泛指非关系型的数据库
NoSQL不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力
-
不遵循SQL标准
-
不支持ACID
-
远超于SQL的性能
用处:
-
对数据高并发的读写
-
海量数据的读写
-
对数据高、可扩展性
2、Redis概述
安装
首先解压redis压缩包
sudo tar -zxvf (redis压缩包)
安装gcc + make
安装过的可以跳过
安装redis的时候需要C语言的编译环境
所以首先安装gcc
//查看gcc版本
gcc --version
//gcc
sudo apt-get install gcc
//make
sudo apt-get install make
在redis目录中执行make命令进行编译
cd (redis文件目录)
make
make时报错
如果没有准备好C语言编译环境,make会报错——Jemalloc/jemalloc.h:没有那个文件
//清除redis中的c文件
make distclean
//再次make
make
执行安装
//在redis文件中继续执行
make install
安装完成之后默认目录:/usr/local/bin
安装成功的文件介绍
redis-benchmark:性能测试工具
redis-check-aof:修复有问题的AOF文件
redis-check-dump:修复有问题的dump.rdb文件
redis-sentinel:Redis集群使用
redis-server:Redis服务器启动命令
redis-cli:客户端,操作入口
后台启动Redis服务
//把redis目录中的redis.conf文件复制到/etc/文件夹下的redis.conf
sudo cp redis.conf /etc/redis.conf
进入etc目录把daemonize no改成yes
sudo vim redis.conf
进入文件中“/”可以搜索字段
/daemonize
回车
把no改成yes
启动redis服务
//在etc/bin目录下输入命令
redis-server /etc/redis.conf
//查看redis启动状态
ps -ef | grep redis
redis-cli启动客户端
redis介绍
redis中默认有16个数据库,类似数组下标从0开始,初始默认使用0号库
使用命令select <dbid>来切换数据库。如:select 8
同一密码管理,所有库同样密码
3、命令和数据类型
数据库操作命令
//查看挡墙库所有key
keys *
//判断某个key是否存在
exists <key>
//查看你的key时什么类型
type <key>
//删除指定的key数据
del <key>
//根据value选择非阻塞删除(仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作)
unlink <key>
//为给定的key设置过期时间
expire <key> 10 //10秒钟
//查看还有多少秒过期,-1表示永不过期,-2表示已过期
ttl <key>
//命令切换数据库
select <index>
//查看当前数据库的key的数量
dbsize
//清空当前库
flushdb
//通杀全部库
flushall
//关闭redis的连接
shutdown
//获取最后一次成功执行快照的时间
lastsave
//产生dump.rdb文件,但里面是空的,无意义
flushall
//使用命令查看redis进程
ps -ef | grep redis
//查看当前客户端redis的主从复制信息
info replication
【role:master表示当前为主服务器,role:slave表示当前为从服务器】
【connected_slaves:0表示当前主服务器有多少子服务器(从服务器)】
【master_host:127.0.0.1主服务器的ip地址】
【master_port:6379主服务器的端口号】
//在redis中输入命令来查看集群信息
cluster nodes
redis常用五大数据类型
-
String(字符串)
-
list(链表)
-
set(集合)
-
zset(sorted set --有序集合)
-
hash(哈希类型)
1、字符串(string)
//添加字符串类型的数据
set <key> <value>
//查询键对应的值
get <key>
//将给定的新值追加到原值的末尾
append <key> <value>
//获得值得长度
strlen <key>
//如果键存在不做任何操作,如果不存在就创建key并设置值
setnx <key> <value>
//同时设置一个或多个key-value
mset <key1> <value1> <key2> <value2>...
//同时获取一个或多个value
mget <key1> <key2> <key3>...
//同时设置一个或多个key-value,仅当所有给定key都不存在
msetnx <key1> <value1> <key2> <value2>...
[原子性,有一个失败则都失败]
//获得值得范围,类似Java中的substring,前包,后包
getrange <key> <起始位置> <结束位置>
//用value覆写key所存储得字符串值,从<起始位置>开始(索引从0开始)
setrange <key> <起始位置> <value>
//设置键值的同时,设置过期时间,单位秒
setex <key> <过期时间> <value>
//以新换旧,设置了新值同时获得旧值
getset <key> <value>
数值操作命令
//将key中存储得数字值加1(只能对数字值操作,如果为空,新增值为1)
incr <key>
//将key中存储得数字值减1(只能对数字值操作,如果为空,新增值为-1)
decr <key>
//将key中存储的数字值加上指定数
incrby <key> <自定义数值>
//将key中存储的数字值减去指定数
decrby <key> <自定义数值>
2、列表(List)
简介
list是个单键多值的操作
Redis列表是简单的字符串列表,按照插入顺序排序
底层是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差(查询效率比较低)
常用命令
//从左边/右边插入一个或者多个值
lpush/rpush <key> <value1> <value2> <value3>...
//从左边/右边吐出一个值。值在键在,值光键亡
lpop/rpop <key>
//从<key1>列表右边吐出一个值,插到<key2>列表左边
rpoplpush <key1> <key2>
//按照索引下标获得元素(从左到右)
lrange <key> <start> <stop>
[获取当前key的全部值]
lrange <key> 0 -1
//按照索引下标获得元素(从左到右)
lindex <key> <index>
//获得列表长度
llen <key>
//在value的后面插入一个新值(before之前/after之后)
linsert <key> before <value> <newvalue>
//从左边删除n个value(从左到右)
lrem <key> <n> <value>
//将列表key下标为index的值替换成value
lset <key> <index> <value>
数据结构
List的数据结构为快速链表quickList
在元素较少的情况下它将所有的元素紧挨着一起存储,使用一块连续的内存存储,这个结构是ziplist,也是压缩列表
当数据量比较多的时候Redis将链表和ziplist结合起来组成了quicklist
3、集合(Set)
简介
set和list类似都是一个列表的功能,但set是可以自动排重的,存储的数据不能重复,并且判断某个成员是否在一个set集合内的重要接口
set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加、删除、查找的复杂度都是O(1)
常用命令
//将一个或多个member元素加入到集合key中,已经存在的member元素将被忽略
sadd <key> <value1> <value2>...
//取出该集合的所有值
smembers <key>
//判断集合<key>是否为含有该<value>值,有1,没有0
sismember <key> <value>
//返回该集合的元素个数
scard <key>
//删除集合中的某个元素
srem <key> <value1> <value2>
//随机从该集合中吐出一个值
spop <key>
//随机从该集合中取出n个值。不会从集合中删除
srandmember <key> <n>
//把集合中一个值从一个集合移动到另一个集合
smove <source> <destination> value
//返回两个集合的交集元素
sinter <key1> <key2>
//返回两个集合的并集元素
sunion <key1> <key2>
//返回两个集合的差集元素(key1中的,不包含key2中的)
sdiff <key1> <key2>
数据结构
set数据结构是dict字典,字典使用哈希表实现的
跟Java中HashSet一样,HashSet所有的value指向同一个对象,set所有的value指向同一个内部值
4、哈希(Hash)
简介
hash是一个键值对集合,也就是一个key对应着一个对象
hash是一个String类型的field和value的映射表,hash特别适合用户存储对象,类似于Java里的map<String,Object>
常用命令
//给key集合中的field键赋值value
hset <key> <field> <value>
//从key集合中的<field>取出value
hget <key> <field>
//批量设置hash的值
hmset <key> <field1> <value1> <field2> <value2>...
//查看哈希表key中,给定域field是否存在
hexists <key1> <field>
//列出该hash集合的所有field
hkeys <key>
//列出该hash集合的所有value
hvals <key>
//为哈希表key中的域field的值加上增量1 -1
hincrby <key> <field> <increment>
//将哈希表key中的域field的值设置为value,当且仅域field不存在
hsetnx <key> <field> <value>
数据结构
hash类型的field-value长度较短且个数较少时,使用ziplist(压缩列表),否则使用hashtable(哈希表)
5、有序集合(Zset)
简介
zset与set非常相似,是一个没有重复元素的字符串集合
不同之处在于zset的每个成员都关联了一个评分(score)这个评分用来从低到高的方式进行排序。集合的成员是唯一的,评分是可重复的
常用命令
//将一个或多个member元素以其score值加入到有序集合key当中
zadd <key> <score1> <value1> <score2> <value2>...
//返回有序集key中,下标在<start> <stop>之间的元素[withscores]:分数和值一起显示
zrange <key> <start> <stop> [withscores]
//返回有序集合key中,所有score值介于min和max之间(包括等于min或max)的成员。有序集成员按score值递增(从小到大)次序排列
zrangebyscore <key> <min> <max> [withscores] [limit offset count]
//同上,改为从大到小排序
zrevrangebyscore <key> <max> <min> [withscores] [limit offset count]
//为元素的score加上增量
zincrby <key> <increment> <value>
//删除该集合下,指定值的元素
zrem <key> <value>
//统计该集合,分数区间内的元素个数
zcount <key> <min> <max>
//返回该值在集合中的排名,从0开始
zrank <key> <value>
数据结构
zset是个特别的数据结构,有两个部分组成,一方面它相当于Map<String,Double>,每个元素value中还赋予了一个权重score,又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的方位来获取元素的列表
zset底层使用了两个数据结构
hash:关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值
跳跃表:跳跃表的目的在于给元素value排序,根据score的范围获取元素列表
4、Redis配置文件
配置文件目录:/etc/redis.conf
Units 单位
配置大小写单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit大小写不敏感
includes
类似于jsp中的include,多实例的情况可以把公用的配置文件提取出来
网路相关配置
bind
默认是bind=127.0.0.1表示只能接受本地访问请求
不写表示无限制,接受任何ip地址的访问
生产环境肯定要写你的应用服务器的地址;服务器是需要远程访问的,所以需要将其注释掉
如果开启了protected-mode,那么在没有设定bind ip且没有设密码的情况下,Redis只允许接受本机的响应
把protected-mode保护模式改成no
port
redis默认启动的端口号6379
可以修改成其他端口号
tcp-backlog
tcp是网路的链接协议,通过三次握手进行链接,四次挥手释放链接
backlog:一个连接队列,backlog队列值(backlog默认值511)=未完成三次握手列队+已经完成三次握手列队
timeout
设置redis的链接超时时间(单位:秒)
默认是0,0表示永不超时
tcp-keepalive
默认值是300,表示每隔三百秒检测一次链接是否还活着,如果活着继续执行服务,如果不活着就释放连接
general
daemonize
设置redis服务为后台启动,默认是no,改为yes为后台启动
pidfile
redis每次操作都会有继承号(端口号),pidfile指定一个pid文件路径,把继承号设置到文件中
loglevel
表示redis中日志的级别,它的值分别是:默认是notice
debug:可以看到更详细的信息
verbose:一些有用的信息
notice:在生产环境中用它
waming:显示一些有用或者重要的信息
logfile
设置日志的输出文件路径,默认为空
可以自己设置日志路径,可以把日志写进设置的文件中
databases
默认值为16,表示默认有16个库
security
设置密码
访问密码的查看、设置和取消
在redis中使用命令设置密码:
在命令中设置密码,只是临时的,重启redis服务器,密码就还原了
永久设置,需要再配置文件中进行设置
limits限制
maxclients
设置当前客户端的最大链接数,默认是10000
maxmemory
建议必须设置,否则,将内存占满,照成服务器宕机
设置redis可以使用的内存量,一旦达到内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定
-
maxmemory-policy移除数据的规则:
-
volatile-lru:使用LRU算法移除key,只对设置了过期时间的键;(最近最少使用)
-
allkeys-lru:在所有集合key中,使用LRU算法移除key
-
volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
-
allkeys-random:在所有集合key中,起初随机的key
-
volatile-ttl:移除哪些TTL值最小的key,即哪些最近要过期的key
-
noeviction:不进行移除,针对写操作,只是返回错误信息
-
-
maxmemory-samples:设置样本数量。
RDB
dir ./
设置RDB文件默认保存路径
dbfilename dump.rdb
设置RDB备份文件的名称
save 3600 1 save 300 100 save 60 10000
设置RDB保存的格式:save <相隔的秒数> <写入几条数据>
save ""
默认为save
save为手动持久化,bgsave为自动持久化
【save:只管保存,其他不管,全部阻塞。手动保存。不建议】 【bgsave:Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求】
stop-writes-on-bgsave-error yes
默认为yes
当Redis无法写入磁盘(磁盘满了)的话,直接关掉Redis的写操作。
rdbcompression
用来压缩文件,对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会蚕蛹LZF算法进行压缩
如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能。推荐yes
rdbchecksum
检查数据完整性,默认为yes,数据持久话的时候来检查数据是否是完整性,还可以让redis使用CRC64算法来进行数据校验
但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能
AOF
appendonly
开启AOF数据的备份
appendfilename "appendonly.aof"
设置AOF格式的备份文件名
appendfsync always
始终同步,每次Redis的写入都会立刻记入日志。可以保证数据的完整性,但是比较消耗性能
appendfsync everysec
每秒同步,每一秒都会记录日志,但是如果宕机,本秒的数据可能会丢失
appendfsync no
redis不主动进行同步,把同步时机交给操作系统
集群
cluster-enabled yes
打开集群模式
cluster-config-file nodes-6379.conf
设定节点配置文件名
cluster-node-timeout 15000
设定节点失联时间,超过该事件(毫秒),集群自动进行主从切换
5、发布和订阅
redis中的发布订阅(pub/sub)其实就是一种的消息的通信方式,发送者(pub)发送消息,订阅者(sub)可以接受到发送的消息。
redis客户端可以订阅任意数量的频道
发布订阅命令(频道名需要一致)
订阅
//在订阅客户端中订阅频道
subscribe <频道name>
发布
//在发布客户端中往频道中发布消息
publish <频道name> <消息>
6、新数据类型
1、Bitmaps
简介
其实就是针对"位"来进行操作一个类型,Bitmaps本身不是一个数据类型,他是一个字符串。它可以对字符串的位进行操作,它和字符串还不太一样,它里面的存储单元是0和1。也可以把它想象成以位为单位的数组,这个数组中只能放0和1,数组的下标叫做偏移量是一个术语
缺点:在初始化的时候,偏移量如果非常大,那么初始化过程比较慢,可能会造成redis的阻塞
命令
setbit:添加数据
//设置Bitmaps中某个偏移量的值(0或1) setbit <key> <offset偏移量> <value> //id=8的这个用户在2020-11-06当天点击页面时redis中执行命令 setbit users:2020-11-06 8 1
getbit:显示数据
//获取Bitmaps中某个偏移量的值 getbit <key> <offset偏移量> //获取id=8的用户是否在2020-11-06这天访问过,返回0说明没有访问过。 getbit users:2020-11-06 8
bitcount:统计字符串被设置为1的bit数量
//统计字符串从start字节到end字节比特值为1的数量 bitcount <key> <start end> //计算2022-11-06这天的独立访问用户数量 bitcount users:2022-11-06
bitop:做一些复合操作
//可以做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中 bitop (and/or/not/xor) <destkey> [key...] setbit users:20201103 1 1 setbit users:20201103 2 1 setbit users:20201103 5 1 setbit users:20201103 9 1 setbit users:20201104 0 1 setbit users:20201104 1 1 setbit users:20201104 4 1 setbit users:20201104 9 1 //计算出两天都访问过网站的用户数量 bitop and users:20201104_03 users:20201104 users:20201103 复合操作 新名字 旧名字 旧名字
Bitmaps与set对比
Bitmaps每个用户id占用空间为1位,存储的用户量可以达到1亿,占用的内存为12.5mb
set每个用户id占用64位,
7、Jedis操作Redis
导入依赖
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
</dependencies>
Java代码
package com.exampl.jedis;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class JedisDemo1 {
static {
Jedis jedis = new Jedis("127.0.0.1",6379);
String value = jedis.type("k2");
System.out.println(value);
//查看redis中全部key
Set<String> keys = jedis.keys("*");
for(String key : keys){
System.out.println("key *:"+key);
}
}
//创建Jedis对象
Jedis jedis = new Jedis("127.0.0.1",6379);
public static void main(String[] args) {
//创建Jedis对象
Jedis jedis = new Jedis("127.0.0.1",6379);
//测试
String ping = jedis.ping();
System.out.println(ping);
}
//操作String
@Test
public void demo1(){
//添加
jedis.set("name","lucy");
//获取
String name = jedis.get("name");
System.out.println("name的值:"+name);
//查看过期时间
Long ttl = jedis.ttl("name");
System.out.println("过期时间:"+ttl);
//设置过期时间
Long expire = jedis.expire("name", 50);
System.out.println("设置过期时间:"+expire);
//查看过期时间
Long ttl1 = jedis.ttl("name");
System.out.println("过期时间:"+ttl1);
//设置多个key-value
jedis.mset("k1","v1","k2","v2");
List<String> mget = jedis.mget("k1", "k2");
System.out.println(mget);
}
//操作list
@Test
public void demo2(){
jedis.lpush("key1","lucy","mary","jack");
List<String> key1 = jedis.lrange("key1", 0, -1);
System.out.println(key1);
}
//操作set
@Test
public void demo3(){
jedis.sadd("name","lucy","jack");
Set<String> names = jedis.smembers("name");
System.out.println(names);
}
//操作hash
@Test
public void demo4(){
jedis.hset("users","age","20");
String hget = jedis.hget("users", "age");
System.out.println(hget);
//设置多个hash类型数据
Map<String,String> map = new HashMap<>();
map.put("name","admin");
map.put("age","18");
map.put("address","beijing");
jedis.hmset("user1",map);
List<String> users = jedis.hmget("user1","name","age","address");
for (String value : users) {
System.out.println(value);
}
}
//操作zset
@Test
public void demo5(){
jedis.zadd("china",100d,"shanghai");
jedis.zadd("china",90d,"beijing");
jedis.zadd("china",80d,"chengdu");
jedis.zadd("china",70d,"guangzhou");
jedis.zadd("china",60d,"shenzhen");
Set<String> china = jedis.zrange("china", 0, -1);
for (String value : china) {
System.out.println(value);
}
}
}
8、SpringBoot整合Redis6
添加redis所需依赖
//redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.4</version>
</dependency>
//spring2.X集成redis所需common-pool2连接池
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
添加配置文件
#Redis 服务器地址
spring.redis.host=localhost
#Redis 服务器链接端口
spring.redis.port=6379
#Redis 数据库索引(默认为0)
spring.redis.database=0
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
编写redis配置类
package com.example.rs.config;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
@EnableCaching
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
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);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
编写controller进行测试
package com.example.rs.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping
public String testRedis(){
//设置值到redis
redisTemplate.opsForValue().set("name","lucy");
//从redis获取值
String value = (String)redisTemplate.opsForValue().get("name");
return value;
}
}
9、Redis中的事务
redis中的事务和mysql中的事务不一样,redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执。事务在执行的过程中,不会被其他客户端发来的命令请求打断。
redis事务的主要作用就是串联多个命令防止别的命令插队
1、操作事务
从输入Multi命令开始,输入的命令都会一次进入命令队列中,但不会执行,直到输入Exec后,redis会将之前的命令队列中的命令依次执行
multi事务的开启
开启事务的组队,开启之后输入命令放入队列中
Exec事务的执行
执行当前队列中的命令,在队列中的命令会按照顺序依次去执行
discard取消事务
类似于Java中的回滚事务,但本质上还是不一样,在组队的过程中可以通过discard来放弃组队
2、Multi、Exec、discard命令
//开启事务队列
multi
//在队列中输入命令
如:set key1 value1
set key2 value2
//执行事务队列
exec
//放弃当前的组队
discard
3、事务的错误处理
1、输入命令时错误
在组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消
//开启队列
multi
//输入命令
set key1 value1
set key2 //没有设置值
//提交事务
exec
组队失败,取消当前的队列,命令都不执行
2、执行时错误
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会被执行,不会回滚
//开启队列
multi
//输入命令
set key1 value1
incr c1 //进行加一操作
//提交事务
exec
组队成功,但是提交时有的命令可以执行有的命令不可以执行
4、解决事务冲突
如果你的账户中有1万块钱,有三个人同时登录你的账号去购物,其中A花8千,B花5千,C花2千,这时就会因为钱不够而导致事务的冲突。
解决办法:
1、悲观锁
介绍
悲观锁(Pessimistic Lock):顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿到这个数据就会block(阻塞)直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁
在操作事务之前上锁
2、乐观锁
介绍
乐观锁(Optimistic Lock):顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set(检查和设置)机制实现事务的
在操作事务的时候不会去上锁,完成事务之后去跟数据进行版本号的比较
用乐观锁操作redis事务
watch
使用watch来监视一个或多个key,如果在事务执行之前这个key配其他命令锁改动,那么事务将被打断:
//在a、b终端中监视同一个key
watch key
//在a、b中开启事务
multi
//在a终端进行减10操作
decrby key 10
//在b终端进行加10操作
incrby key 10
//提交
exec
如果先在a终端提交,那么在b终端提交的时候事务被打断
如果现在b终端提交,那么在a终端提交的时候事务被打断
说明:在a提交时更新数据的版本,所以在b中找不到所对应的版本数据。
unwatch
取消watch命令对所有key的监视。如果在执行watch命令之后,exec命令或discard命令先被执行了的话,那么就不需要再执行unwatch了。
3、事务三特性
1、单独的隔离操作
事务中的所有命令都会被序列化、按照顺序地执行。事务再执行的过程中,不会被其他客户端发送来的命令请求所打断
2、没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
3、不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
10、持久化——RDB
在redis中的默认持久化文件格式为RDB:dump.rdb
RDB(Redis DataBase):在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里
**【在启动时把保存的快照文件直接读到redis内存中】
持久化过程:Redis会单独创建一个子进程(fork)来进行持久化,先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效,RDB的缺点是最后一次持久化后的数据可能丢失。
【比如,设置30秒持久化10条数据。持久化了10条数据,我再次更新了5条数据,不够10条,这时如果redis崩溃了,那么这5条数据将不会被持久化,这样就会造成持久化的数据丢失】
fork:复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
一般情况父进程和子进程会共用同一段物理内存,再进程空间发生变化时,才将父进程的内容复制到子进程中,fork是将整个文件替换
RDB备份与恢复
备份
在redis启动时,会根据配置文件中的配置来进行,操纵加载备份过的文件(dump.rdb),默认加载的rdb备份文件为:dump.rdb
可以使用cp命令来复制一份dump.rdb文件来进行备份
//备份
cp dump.rdb dump.rdb.bak
恢复
把备份的文件拷贝到配置文件指定的工作目录下,修改文件名为dump.rdb。然后启动redis就会自动加载备份的数据
优势和劣势
优势
-
适合大规模的数据恢复
-
对数据完整性和一致性要求不高更合适使用
-
节省磁盘空间
-
恢复速度快
劣势
-
fork的时候,内存中的数据被克隆了一份。大致2倍的膨胀性需要考虑
-
在写时拷贝时,如果数据量庞大,比较消耗性能
-
在做最后一次备份操作的时候,如果redis意外down掉的话,就会丢失数据
11、持久化——AOF
AOF(Append Only File):以日志的形式来记录每个写操作(增删改操作,改变内容的操作),将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
AOF默认不开启
可以在redis.conf中配置AOF文件,默认为appendonly.aof,AOF文件的保存路径,同RDB的路径一致
如果RDB和AOF同时开启,系统默认取AOF的数据(数据不会存在丢失)
AOF备份和恢复
备份
AOF和RDB的备份方式是一样的
恢复
重启redis会自动加载AOF备份文件
异常恢复:
如果遇到AOF文件损坏,就会导致redis连接不上,可以通过/usr/local/bin/redis-check-aof --fix来进行恢复AOF文件
//在终端输入命令来修复
redis-check-aof --fix appendonly.aof
12、主从复制
主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slaver以读为主
好处:
-
读写分离,性能扩展。
-
主服务器负责写操作,从服务器负责读操作
-
-
容灾快速恢复
-
如果一台从服务器宕机,可以读取其他的从服务器
-
如果主服务器宕机了,可以使用集群解决
主从复制原理
1、当从服务器连接上主服务器之后,从服务器会向主服务器发送进行数据同步消息
2、主服务器接到从服务器传过来的同步消息之后,把主服务器数据进行持久化成rdb文件,把rdb文件再发送给从服务器,从服务器拿到rdb进行读取就完成了主从复制操作
3、每次主服务器进行写操作之后,主服务器会自动的进行复制数据到从服务器中进行同步
一主多从
操作
1、在根目录下创建一个myredis文件夹
sudo mkdir /myredis
文件夹必须建立在跟目录下
2、复制/etc/redis.conf文件到myredis文件夹中
sudo cp /etc/redis.conf /myredis/redis.conf
//用vim命令进到/myredis/redis.conf文件夹中
sudo vim /myredis/redis.conf
把appendonly的值改成no或者修改名字
3、创建三个配置文件并写入配置
//在/myredis文件夹中创建三个配置文件
sudo touch redis6379.conf redis6380.conf redis6381.conf
4、使用vim命令分别进入文件写入配置
include /myredis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb
#include指定一个公共的配置文件
#pidfile指定位置生成pid文件(修改成自己的pid文件名称)
#post设置redis启动端口(修改成自己的端口号)
#dbfilename设置当前redis端口的RDB快照(修改成自己的RDB快照名称)
5、分别指定配置文件来启动redis服务
redis-server /myredis/redis6379.conf
redis-server /myredis/redis6380.conf
redis-server /myredis/redis6381.conf
//使用命令查看redis进程
ps -ef | grep redis
//启动三个终端分别启动三个redis客户端
redis-cli -p <端口号>
//查看当前客户端redis的主从复制信息
info replication
【role:master表示当前为主服务器,role:slave表示当前为从服务器】
【connected_slaves:0表示当前主服务器有多少子服务器(从服务器)】
【master_host:127.0.0.1主服务器的ip地址】
【master_port:6379主服务器的端口号】
6、主从连接
//在从服务器中输入命令进行连接
slaveof <主ip地址> <主端口号>
主写从读
从服务器中不能执行写操作,修改数据的写操作都写在主服务器中,从服务器中都是执行的读操作
注意
当一个从服务器down之后,主服务器的子服务器数量就会减一,再次启动时,当前的从服务器会变成一个主服务器,需要手动进行连接配置主服务器才能继续进行主从复制。
当一个主服务器down之后,其他的从服务器知道主服务器down掉了,但是还是会跟主服务器的端口号连接着,等到主服务器恢复之后,从服务器不变
薪火相传
上一个Slave可以是下一个slave的Master,Slave同样可以接收其他slaves的连接和同步请求,那么该slave作为了链条中下一个的master,可以有效减轻master的写压力,去中心化降低风险
【一个从服务器还可以做其他从服务器的主服务器,但是不能进行写操作,因为还是一个从服务器,写操作只能在主服务器中进行】
中途变更会转向:会清除之前的数据,重新建立拷贝最新的
风险是一旦某个slave宕机,后面的slave都没法备份
主机挂了,从机还是从机,无法写数据了
反客为主
当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改
用slaveof no one命令将从机变为主机
缺点:不能自动实现,需要手动进行
哨兵模式
反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库
给主机派一个哨兵来进行监听主机,如果主机故障则通知一个从机来做主机
注意
-
从机选举新的主机后,原主机重启后会变成从机
-
选从为主的条件:
-
1、选择优先级靠前的
-
优先级在redis.conf配置文件中:replica-priority 100,值越小优先级越高
-
-
2、选择偏移量最大的
-
偏移量是指获得原主机数据最全的
-
-
3、选择runid最小的从服务
-
每个redis实例启动后都会随机生成一个40位的runid
-
-
操作
//在/myredis目录下创建sentinel.conf文件并编写配置,名字绝不能错
sentinel monitor mymaster 127.0.0.1 6379 1
【mymaster:监控对象起的服务器名称,1:至少有1个哨兵同意迁移】
//在终端启动哨兵
redis-sentinel /myredis/sentinel.conf
复制延时
由于所有的写操作都是先在Master上操作,然后同步更新到Slave上, 所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重
13、集群
Redis集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。
Redis集群通过分区(partition)来提供一定程度的可用行(availability):即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求。
无中心化集群
在集群多台服务器中随机一台服务器就可以作为集群的入口,每个服务器之间都是相互连通的,通过请求找到对应的服务器来进行操作
搭建集群
删除/myredis目录中的rdb、aof文件
sudo rm -r /myredis/dump63*
//*表示以dump63开头的文件/文件夹全部删除
修改/myredis目录下的每个配置文件
//主从复制的默认配置
include /myredis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb
//添加集群配置
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000
复制配置文件
sudo cp redis6379.conf redis6380.conf
sudo cp redis6379.conf redis6381.conf
sudo cp redis6379.conf redis6389.conf
sudo cp redis6379.conf redis6390.conf
sudo cp redis6379.conf redis6391.conf
vim进入每个配置文件并修改
//在vim中使用命令来进行修改
:%s/6379/6380
:%s/6379/6381
:%s/6379/6389
:%s/6379/6390
:%s/6379/6391
//说明: :%s/旧值/新值
启动服务并把6个节点合成一个集群
redis-server /myredis/redis6379.conf
redis-server /myredis/redis6380.conf
redis-server /myredis/redis6381.conf
redis-server /myredis/redis6389.conf
redis-server /myredis/redis6390.conf
redis-server /myredis/redis6391.conf
//使用命令进行启动集群
redis-cli --cluster create --cluster-replicas 1 host:port host:port host:port host:port host:port host:port
【host:需要用真实的IP地址】
【--replicas 1:采用最简单的方式配置集群,一台主机,一台从机,正好三组】
回车之后会提示是否接收这种方式,输入yes
//连接redis
redis-cli -c -p 6379
//在redis中输入命令来查看集群信息
cluster nodes
注意
一个集群至少有三个主节点
选项--cluster-replicas 1表示我们希望为集群中的每个主节点创建一个从节点
分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上
插槽
一个redis集群包含了16384个插槽(hash slot)数据库中的每个键都属于这16384个插槽的其中一个,redis集群把这些插槽均衡的分配给每个服务器(节点),如:有三个节点
-
节点A负责处理0~5460个插槽
-
节点B负责处理5461~10922个插槽
-
节点C负责处理10923~16383个插槽
插入数据
添加一条数据
set k1 v1 #返回k1的插槽值以及插槽所在的端口
添加多条数据
mset name{user} lucy age{user} 20
#添加多条数据时需要设置组,在{}中设置组
查看key的插槽值
cluster keyslot <key>
#返回插槽值
查看插槽中key的数量
cluster countkeysinslot <slot>
#返回key的数量,没有数据返回0
#需要进到当前slot所在的端口节点中
查看插槽中指定key的数量值
cluster getkeysinslot <slot> <数量>
#返回指定数量的值
注意
如果主节点宕机了,子节点自动替换成主节点,当原主节点再次启动时,就会变成当前主节点的子节点
如果某节点的主从都宕掉了,在配置文件中的cluster-require-full-coverage的值为yes,那么整个集群都会宕掉。如果为no,那么该节点数据全部不能使用,也无法存储,其他节点的服务器都正常运行
Java操作集群
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
public class JedisClusterTest{
public static void main(String[] args){
//创建对象
HostAndPort hostAndPort = new HostAndPort("127.0.0.1",6379);
JedisCluster jedisCluster = new JedisCluster(hostAndPort);
//进行操作
jedisCluster.set("b1","value1");
String value = jedisCluster.get("b1");
System.out.println("value:"+value);
jedisCluster.close();
}
}
14、redis应用问题
缓存穿透
概念
用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询,发现也没有,于是本次查询失败。当用户很多的时候【比如:秒杀、热门等】,缓存都没有命中,于是都去请求了持久层数据库,这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
解决方案
1、布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力
请求过来的时候先经过过滤器,不符合的请求被拦截下来,不会进到服务器后台
2、缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后在访问这个数据将会从缓存中获取,保护了后端的数据源
但是这种方法会存在两个问题:
-
如果控制能够被缓存器来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多空值的键
-
即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响
缓存击穿
概念
这里需要注意和缓存击穿的区别,缓存击穿指的是一个key非常热点,在不停的扛着大并发,大并发集中的对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
当某个key在过期的瞬间,有大量的请求并发访问, 这类数据一般是热点数据,由于缓存过期,会同时范文数据库来拆线呢最新数据,并且回写缓存,会导使数据库瞬间压力过大
解决方案
设置热点数据永不过期
从缓存层面来看,没有设置过期时间,所以不会出现热点key过期后产生的问题
-
虽然解决了击穿问题,但是会占用大量的空间,redis的空间满了之后会自动清理这些key
加互斥锁
分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种凡是将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
缓存雪崩
概念
缓存雪崩,是指在某一个时间段,缓存集中过期失效,Redis宕机
产生雪崩的原因之一,比如在写本文的时候,马上就要到双十一零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了,而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况
其实际中过期,倒不是非常知名的缓存雪崩,使缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的,无非就是对数据库产生周期性的压力而已,而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮
解决方案
redis高可用
这个思想的含义是,既然redis有课能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活)
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来空值读数据库写缓存的线程数量,比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能用到的数据预先访问一边,这样部分可能大量访问的数据就会加载到缓存中,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
ab模拟并发测试工具
ab命令会创建多个并发访问线程,模拟多个访问者同时对某一URL地址进行访问。它的测试目标是基于URL的,因此,它既可以用来测试apache的负载压力,也可以测试nginx、lighthttp、tomcat、IIS等其他web服务器的压力
//安装ab
sudo apt-get install apache2-utils
命令
//查看ab的命令说明文档
ab --help
//发送的请求数
-n requests Number of requests to perform宅
//一次同时并发的请求数,总的请求数(n)=次数*一次并发数(c)
-c concurrency Number of multiple requests to make
//设置post提交
-T content-type Content-type header to use for POST/PUT data
//指定一个携带post参数数据的文件
-p postfile File containing data to POST. Remember also to set -T
//对 http://www.meetu.hk/ 进行100次请求,10个并发请求压力测试结果
ab -n 100 -c 10 http://www.meetu.hk/