1.NoSQL数据库简介
多服务器的用户登录session对象如何存储?
session复制到不同的服务器会造成数据冗余
session存储在文件系统或关系型数据库中IO开销大
session存储在cookie中安全性差
session存储在redis中,速度快,存在内存中不需要IO
NoSQL=not only SQL泛指非关系型数据库
不遵循SQL标准
不支持ACID
远超SQL的性能
NoSQL适用于海量数据,高并发,数据高可扩展
NoSQL不适用于事务支持,基于SQL的结构化查询
实际中用了SQL也不行或者用不着SQL时可以考虑NoSQL
2.Redis概述安装
1.开源k-v数据库
2.支持存储的V类型更多,包括string,list,hash,set,zset等
3.数据类型支持各种操作且都是原子性的
4.支持不同方式的排序
5.数据为了保证效率存在内存中,同时支持持久化
6.实现了主从同步
1)多样的数据结构以及实际应用
最近N个数据:通过List实现按自然时间排序的数据
排行榜:zset(有序集合)
验证码:Expire
计数器,秒杀:原子性,自增方法INCR,DECR
去重:set
构建队列:list
发布订阅消息系统:pub/sub模式
2)安装
#在CentOS 7.X下安装C 语言的编译环境
yum install centos-release-scl scl-utils-build
yum install -y devtoolset-8-toolchain
scl enable devtoolset-8 bash
#测试gcc版本
gcc --version
#或者直接安装gcc
yum install gcc
#解压redis压缩包
tar -zxvf redis-6.2.1.tar.gz -C /opt/software/
#进入redis目录中使用make编译 编译完成后使用make install完成安装
cd redis-6.2.1/
make
make install
#结束后默认安装在/usr/local/bin/
redis-benchmark:性能测试工具,可以在自己本子运行,看看自己本子性能如何
redis-check-aof:修复有问题的AOF文件,rdb和aof后面讲
redis-check-dump:修复有问题的dump.rdb文件
redis-sentinel:Redis集群使用
redis-server:Redis服务器启动命令
redis-cli:客户端,操作入口
3)可能遇到的错误
Jemalloc/jemalloc.h:没有那个文件
如果没有准备好C语言编译环境,make会报—Jemalloc/jemalloc.h:没有那个文件
解决方案:运行make distclean 完成后再次make编译
4)Redis启动
①前台启动,不推荐
cd /usr/local/bin/
redis-server
②后台启动,推荐
修改配置文件中daemonize yes
redis-server /opt/module/redis-6.2.1/redis.conf 启动
redis-cli 连接redis服务
5)Redis简单介绍
①默认端口6379来源Mrez女明星,默认16个数据库。默认使用0号库 使用select dbid来切换
②统一密码管理,所有库都用同一个密码
③单线程+多路IO复用
3.五大常用数据类型
1)Redis键(key)
keys *:查看当前库全部key,支持模式匹配
exists key:查看key是否存在
type key:查看key类型
del key:删除指定key的数据
unlink key:根据value选择非阻塞删除,真正的删除操作在后续异步执行
expire key 10:设置key过期时间,单位为秒
ttl key:查看还有多少秒过期,-1表示永不过期,-2表示已过期
dbsize 查看当前数据库的key数量 flushdb 清空当前库 flushall 清空所有库
2)Redis字符串(String)
①概述
Sring是二进制安全的,意味着redis的string可以包含任何数据,比如jpg的图片序列化对象,redis中string的value值最多512M
②基本操作
set key value:添加键值对
get key:获取键的值
append key value:将值追加到原值的末尾
strlen key:获取值得长度
setnx key value:只有当key不存在时设置key的值
incr key:将key中存储的数字加一,具有原子性,多线程原子性指不被其他线程打断,redis单线程能在单条指令完成的都具有原子性
decr key:将key中存储的数字值减一,具有原子性
incrby/decrby key 步长:将key中存的数字加或减步长
mset key1 value1 key2 value2…:批量设置kv
mget key1 key2…:批量获取key的value
msetnx key1 value1 key2 value2…:批量设置kv,只有key存在时才设置,且具有原子性,只有所有key都不存在才会成功
getrange key begin after:截取值得范围,类似java的substring,但是包括头也包括尾
setrange key begin value:从指定位置开始覆写值
setex key 过期时间 value:在设置值得时候同时设置过期时间
getset key value:设置新值得同时获取旧值
③数据结构
String数据结构为简单动态字符串,内部结构实现类似于java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6oR8Bun4-1675950071529)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230131215022807.png)]
如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有 的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。
3)Redis列表(List)
①概述
单键多值,按照插入顺序排序,可以插入到列表的头部(左),也可插到列表的尾部(右),底层为双向链表,两端节点性能高,中间差。
②基本操作
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/after value newvalue:在value前或后插入newvalue
lrem key n value:从左到右删除n个value
lset key index value:将列表下标为index的元素替换为value
③数据结构
4)Redis集合(Set)
①概述
Set是String类型的无序集合,他的底层是一个value为null的hash表,所以增删查的时间复杂度是O(1 )
②基本操作
sadd key value1 value2…:将一个或多个元素加入集合key中,已存在的元素将被忽略
smembers key:取出集合中所有值
sismember key value:判断集合key中是否含有value值,有返回1,没有返回0
scard key:返回集合中元素个数
srem key value1 value2…:删除集合中的某些元素
spop key num:随机从集合中吐出num个值,吐出后值删除
srandmember key num:随机从集合中取出num个值,取出后不影响原集合
smove key1 key2 value1:将集合key1中的value1移动到集合key2中
sinter key1 key2:返回两个集合的交集元素
sunion key1 key2:返回两个集合的并级元素
sdiff key1 key2:返回两个集合的差集元素(包含在key1中的,不包含在key2中的)
③数据结构
5)Redis哈希(Hash)
①概述
Redis hash是一个键值对集合,是一个String类型的field和value的映射表,hash适合存储对象,类似Java的Map(String,Object)
②基本操作
hset key field value:给key集合中的field键赋值为value
hget key field:获取key集合中的field的值
hmset key field1 value1 field2 value2:批量插入数据
hexists key field:判断key中field是否存在
hkeys key:列出集合key中所有field
hvals key:列出集合中所有value
hincrby key field num:集合中field字段值加num,num可以为负数
hsetnx key field value::给key集合中的field键赋值为value,当且仅当field不存在时成功
③数据结构
6)Redis有序集合(Zset)
①概述
类似于普通集合,没有重复元素的字符串集合,但每个成员关联了一个score,按照从最低分到最高分的方式排序集合中的成员
②基本操作
zadd key score1 value1 score2 value2…:将一个或多个merber元素及值加入到有序key中
zrange key start stop [withscores]:返回有序集key中,下标在
zrangebyscore key min max [withscores] [limit offset count]:返回集合中score在min和max之间的成员,按score从小到大排列
zrevrangebyscore key min max [withscores] [limit offset count]:同上从大到小排列
zincrby key num value1:给value1的score加上num
zrem key value:删除集合下指定值得元素
zcount key min max:统计集合中min和max之间的个数
zrank key value:返回该值在集合中的排名,从0开始
③数据结构
4.Reids配置文件
#只支持bytes不支持bit,大小写不敏感
######include
#可以通过include引入其他配置文件
include /path/commonconf
######网络相关配置
#默认情况bind 127.0.0.1只能接受本机的访问请求,不写的情况下,无限制接受任何ip地址的访问
#如果开启了protected-mode yes,那么在没有设定bind ip且没有设密码的情况下,Redis只允许接受本机的响应
bind 127.0.0.1
#设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和=未完成三次握手队列 + 已经完成三次握手队列。
#在高并发环境下你需高backlog值来避免慢客户端连接问题。默认为511
tcp-backlog 511
#timeout一个空闲的客户端维持多少秒会关闭,0表示关闭该功能。即永不关闭。
timeout 0
#对访问客户端的一种心跳检测,每个n秒检测一次。单位为秒,如果设置为0,则不会进行Keepalive检测,建议设置成60
tcp-keepalive 0
#####GENERAL通用
#daemonize是否为后台进程,设置为yes ,守护进程,后台启动
daemonize yes
#pidfile存放pid文件的位置,每个实例会产生一个不同的pid文件
pidfile /var/run/redis.pid
#loglevel指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认notice,生产选notice或warning
loglevel notice
#databases设定库的数量 默认16
databases 16
#logfile指定日志文件路径,默认为空
logfile ""
#####SECURITY安全
#requirepass设定密码
requirepass 123456
#####LIMITS限制
#maxclients设置redis同时可以与多少个客户端进行连接,默认10000
maxclients 10000
#maxmemory设置redis可以使用的内存量一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定
#建议必须设置,否则,将内存占满,造成服务器宕机 ,默认不限制
maxmemory <bytes>
#maxmemory-policy 内存使用上限redis移除内部数据移除规则
#volatile-lru:使用LRU算法移除key,只对设置了过期时间的键;(最近最少使用)
#allkeys-lru:在所有集合key中,使用LRU算法移除key
#volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
#allkeys-random:在所有集合key中,移除随机的key
#volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key
#noeviction:不进行移除。针对写操作,只是返回错误信息
5.Redis发布订阅
Reids客户端可以订阅任意数量的频道,并且可以向频道发布消息,所有订阅者都将收到推送
subscribe channelname:订阅该名称的频道
publish channelname message:向该频道发消息
6.Redis6新数据类型
1)Bitmaps
2)Hyperloglog
3)Geospatial
7.Jedis操作Redis
1)连接redis
<!--Jedis需要的依赖-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
public class JedisDemo1 {
public static void main(String[] args) {
//创建Jedis对象
Jedis jedis = new Jedis("192.168.10.100", 6379);
jedis.auth("123456");
String ping = jedis.ping();
System.out.println(ping);
jedis.quit();
jedis.close();
}
}
//注,需要虚拟机关闭防火墙,且redis配置文件中bind注掉或者指定访问ip,protected-mode设为no
//关闭防火墙命令 systemctl stop firewalld
2)使用Jedis操作Redis
public class JedisDemo1 {
public static void main(String[] args) {
//创建Jedis对象
Jedis jedis = new Jedis("192.168.10.100", 6379);
jedis.auth("123456");
Set<String> keys = jedis.keys("*");
jedis.set("name","tom");
jedis.mset("name","tom","age","22");
//使用Jedis对象点方法操作Redis,方法名与Redis命令行中类似
jedis.quit();
jedis.close();
}
}
3)模拟手机验证码发送
//1、输入手机号,点击发送后随机生成6位数字码,2分钟有效
//2、输入验证码,点击验证,返回成功或失败
//3、每个手机号每天只能输入3次
/**
* @program: demo
* @description: 模拟手机验证码发送
* @author: Yosun
* @create: 2023-02-03 22:38
**/
public class JedisDemo2 {
//1、输入手机号,点击发送后随机生成6位数字码,2分钟有效
//2、输入验证码,点击验证,返回成功或失败
//3、每个手机号每天只能输入3次
public static void main(String[] args) throws Exception {
String phone = getPhone();
String code = getCode();
String s = sendCode(phone, code);
System.out.println(check(phone, s));
s = sendCode(phone, code);
System.out.println(check(phone, s));
s = sendCode(phone, code);
System.out.println(check(phone, "123456"));
s = sendCode(phone, code);
System.out.println(check(phone, s));
}
/*
* 获取手机号
* */
public static String getPhone(){
Scanner scanner = new Scanner(System.in);
System.out.print("请输入手机号码:");
return scanner.nextLine();
}
/*
* 生成验证码
* */
public static String getCode(){
Random random = new Random();
String code = "";
for (int i = 0; i <6 ; i++) {
code += random.nextInt(10);
}
return code;
}
/*
* 发送验证码
* */
public static String sendCode(String phone,String code) throws Exception {
Jedis jedis = new Jedis("192.168.10.100", 6379);
jedis.auth("123456");
String count = jedis.get(phone + "count");
if(count ==null){
//还未发送,进行第一次发送
jedis.setex(phone,120,code);
jedis.setex(phone+"count",24*60*60,"1");
}else if (Integer.parseInt(count)>=3){
System.out.println("今日已达到发送上限,请明日再来");
return;
}else {
jedis.setex(phone,120,code);
jedis.incr(phone+"count");
}
jedis.quit();
jedis.close();
return code;
}
/*
* 验证码校验
* */
public static boolean check(String phone,String code){
Jedis jedis = new Jedis("192.168.10.100", 6379);
jedis.auth("123456");
boolean res= code.equals(jedis.get(phone));
jedis.quit();
jedis.close();
return res;
}
}
8.Springboot整合Redis
①引入依赖
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
②application.properties配置redis配置
#设置pom中Springboot版本为2.2.1.RELEASE
#Redis服务器地址
spring.redis.host=192.168.10.100
#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配置类
@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;
}
}
④RedisTestController中添加测试方法
@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping
public String testRedis() {
//设置值到redis
redisTemplate.opsForValue().set("name","lucy");
//从redis获取值
String name = (String)redisTemplate.opsForValue().get("name");
return name;
}
}
9.Redis6的事务和锁机制
1)Redis中事务的定义
redis事务是一个单独的隔离操作,事务中的所有命令都会序列化,按顺序执行,事务在执行过程中不会被其他客户端送来的命令请求所打断,事务的主要作用就是串联多个命令,防止其他命令插队
2)Multi、Exec、discard
从输入multi命令开始,之后输入的命令都将按顺序进入命令队列,但不会执行,直到输入exec后,命令队列中的命令开始按顺序执行,组队的过程中可以通过discard放弃组队。
3)事务的错误处理
如果组队中某个命令出现了错误,则所有命令都不会执行;如果执行阶段产生错误,则只有发生错误的那条执行失败,其他命令依旧会执行。
4)事务的冲突问题
①悲观锁
每次操作都认为有其他人会操作数据,故每次操作前都上锁,解锁后别人才可以操作,缺点是效率低,适用于写多的场景
②乐观锁
为数据加一个版本号,所有人都能得到数据,每个人在修改时不光修改数据,也同步修改版本号,其他人操作时检查当前数据版本号和数据库中版本号是否匹配,适用于读多写少的场景
③乐观锁在Redis的实现
whatch key [key …]:在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key被其他命令所改动,那么事务将被打断。
unwatch:取消 WATCH 命令对所有 key 的监视。如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。
5)秒杀案例
//简单版 存在超时和超卖问题
//加入连接池 解决超时问题
//加入事务-乐观锁 解决超卖问题,但引入库存遗留问题
//使用lua脚本的方式解决库存遗留问题
public class Test {
public static void main(String[] args) {
String userid = new Random().nextInt(50000) +"" ;
String prodid =request.getParameter("prodid");
boolean isSuccess=SecKill_redis.doSecKill(userid,prodid);
boolean isSuccess= SecKill_redisByScript.doSecKill(userid,prodid);
}
}
//普通乐观锁版
public class SecKill_redis {
public static void main(String[] args) {
Jedis jedis =new Jedis("192.168.44.168",6379);
System.out.println(jedis.ping());
jedis.close();
}
//秒杀过程
public static boolean doSecKill(String uid,String prodid) throws IOException {
//1 uid和prodid非空判断
if(uid == null || prodid == null) {
return false;
}
//2 连接redis
//Jedis jedis = new Jedis("192.168.44.168",6379);
//通过连接池得到jedis对象
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
//3 拼接key
// 3.1 库存key
String kcKey = "sk:"+prodid+":qt";
// 3.2 秒杀成功用户key
String userKey = "sk:"+prodid+":user";
//监视库存
jedis.watch(kcKey);
//4 获取库存,如果库存null,秒杀还没有开始
String kc = jedis.get(kcKey);
if(kc == null) {
System.out.println("秒杀还没有开始,请等待");
jedis.close();
return false;
}
// 5 判断用户是否重复秒杀操作
if(jedis.sismember(userKey, uid)) {
System.out.println("已经秒杀成功了,不能重复秒杀");
jedis.close();
return false;
}
//6 判断如果商品数量,库存数量小于1,秒杀结束
if(Integer.parseInt(kc)<=0) {
System.out.println("秒杀已经结束了");
jedis.close();
return false;
}
//7 秒杀过程
//使用事务
Transaction multi = jedis.multi();
//组队操作
multi.decr(kcKey);
multi.sadd(userKey,uid);
//执行
List<Object> results = multi.exec();
if(results == null || results.size()==0) {
System.out.println("秒杀失败了....");
jedis.close();
return false;
}
//7.1 库存-1
//jedis.decr(kcKey);
//7.2 把秒杀成功用户添加清单里面
//jedis.sadd(userKey,uid);
System.out.println("秒杀成功了..");
jedis.close();
return true;
}
}
//lua脚本版
public class SecKill_redisByScript {
private static final org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;
public static void main(String[] args) {
JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis=jedispool.getResource();
System.out.println(jedis.ping());
Set<HostAndPort> set=new HashSet<HostAndPort>();
// doSecKill("201","sk:0101");
}
static String secKillScript ="local userid=KEYS[1];\r\n" +
"local prodid=KEYS[2];\r\n" +
"local qtkey='sk:'..prodid..\":qt\";\r\n" +
"local usersKey='sk:'..prodid..\":usr\";\r\n" +
"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
"if tonumber(userExists)==1 then \r\n" +
" return 2;\r\n" +
"end\r\n" +
"local num= redis.call(\"get\" ,qtkey);\r\n" +
"if tonumber(num)<=0 then \r\n" +
" return 0;\r\n" +
"else \r\n" +
" redis.call(\"decr\",qtkey);\r\n" +
" redis.call(\"sadd\",usersKey,userid);\r\n" +
"end\r\n" +
"return 1" ;
static String secKillScript2 =
"local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +
" return 1";
public static boolean doSecKill(String uid,String prodid) throws IOException {
JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis=jedispool.getResource();
//String sha1= .secKillScript;
String sha1= jedis.scriptLoad(secKillScript);
Object result= jedis.evalsha(sha1, 2, uid,prodid);
String reString=String.valueOf(result);
if ("0".equals( reString ) ) {
System.err.println("已抢空!!");
}else if("1".equals( reString ) ) {
System.out.println("抢购成功!!!!");
}else if("2".equals( reString ) ) {
System.err.println("该用户已抢过!!");
}else{
System.err.println("抢购异常!!");
}
jedis.close();
return true;
}
}
//Jedis连接池
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;
private JedisPoolUtil() {
}
public static JedisPool getJedisPoolInstance() {
if (null == jedisPool) {
synchronized (JedisPoolUtil.class) {
if (null == jedisPool) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200);
poolConfig.setMaxIdle(32);
poolConfig.setMaxWaitMillis(100*1000);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setTestOnBorrow(true); // ping PONG
jedisPool = new JedisPool(poolConfig, "192.168.44.168", 6379, 60000 );
}
}
}
return jedisPool;
}
public static void release(JedisPool jedisPool, Jedis jedis) {
if (null != jedis) {
jedisPool.returnResource(jedis);
}
}
LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。
但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。
利用lua脚本淘汰用户,解决超卖问题。
redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题
10.Redis持久化之RDB
1)什么是RDB
RDB:指定的时间间隔内将内存中的数据集快照写入磁盘
2)备份是如何执行的
Redis会单独创建一个子进程(Fork)进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件,整个过程中,主进程是不进行任何io操作的,确保了高性能,如果需要进行大规模数据恢复吗,且对数据完整性不是特别敏感,那选择RDB要比AOF更加高效。
3)Fork
Fork的作用是复制一个与当前进程一样的进程,新进程的所有数据(变量,环境变量,程序计数器等)与原进程一致,作为原进程的子进程
4)配置
#数据库持久化文件名
dbfilename dump.rdb
#数据库持久化文件路径
dir ./
#当持久化写入操作发生错误时是否继续写入yes :不继续写入
stop-writer-on-bgsave-error yes
#持久化文件是否进行压缩存储,压缩算法为Lzf
rdbcompression yes
#持久化之后是否检查数据完整性 性能消耗大约百分之10,CRC64算法
rdbchecksum yes
#设置何时持久化,redis6默认关闭,save 900 100含义为如果在900秒中有100个key发生了变化,那么久进行持久化 操作
save 900 100
#一般推荐使用bgsave,会在后台进行异步快照操作,快照同时还可以响应客户端请求
bgsave 900 100
#可以通过lastsave命令获取最后一次成功执行快照的时间
5)优势、劣势
优:适合大规模的数据恢复
对完整性、一致性要求不高的场景适用
节省磁盘空间
恢复速度快
劣:Fork时,内存中的数据被克隆了一份,大致2倍的膨胀性需要注意
虽然fork使用了写时拷贝技术,但数据大时性能消耗需要考虑
周期性备份,Redis挂掉的话会损失最后一次快照的所有修改
6)RDB持久化流程图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pmPoOI4g-1675950071531)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230205195847826.png)]
7)RDB备份恢复
将备份好的数据库文件放在备份文件的存放地址,并改成和备份文件相同的名字,启动redis,就可以恢复数据
11.Redis持久化之AOF
1)什么是AOF
全称为Append Only File,以日志的形式记录每个写操作,读操作不记录,只许追加文件但不可以改写文件,redis启动之初会读取该文件将所有指令执行一遍,重新构建数据
AOF默认不开启
#默认不开启
appendonly yes
#生成文件名
appendfilename "appendonly.aof"
2)AOF备份恢复
同RDB
3)AOF异常恢复
在AOF文件损坏后,可以使用启动目录下的redis-check-aof --fix filename进行修复
4)AOF配置
#AOF同步频率设置
#always始终同步,每次写入都立刻记入日志,性能较差,但完整性好
#everysec 每秒一次,宕机会丢失一秒数据
#no 不主动同步,同步时机交给操作系统
appendfsync always
5)Rewrite压缩
重写压缩是什么?
将set a a1
set b b1两条命令压缩为set a a1 b b1
原理
AOF文件过大时,fork一条新进程出来将文件重写(也是先写临时文件再rename)
Redis4.0后的重写 ,是指上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。
#不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
no-appendfsync-on-rewrite=yes
#还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)
no-appendfsync-on-rewrite=no
何时重写
Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发
或者输入命令bgrewriteaof
auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。
AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)
重写流程
(1)bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
(2)主进程fork出子进程执行重写操作,保证主进程不会阻塞。
(3)子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
(4)①.子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。
②.主进程把aof_rewrite_buf中的数据写入到新的AOF文件。
(5)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ln7zi3gB-1675950071532)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230205211541371.png)]
6)优势、劣势
优: 备份机制更稳健,丢失数据概率更低。
可读的日志文本,通过操作AOF稳健,可以处理误操作。
劣:比起RDB占用更多的磁盘空间。
恢复备份速度要慢。
每次读写都同步的话,有一定的性能压力。
存在个别Bug,造成恢复不能。
12.Redis主从复制
1)是什么,以及能干嘛
主机数据更新后根据配置和策略,自动同步到备机的master/slave机制,Master以写为主,Slave以读为主
可以做到读写分离,性能扩展
容灾快速恢复
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZGl76fIw-1675950071533)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230206211845375.png)]
2)怎么玩
①拷贝多个redis.conf文件
②配置一主两从,在配置文件中配置
daemonize yes
pidfile 6379.pid/6380.pdi/6381.pid
port 6379/6380/6381
dbfilename 6379.rdb/6380.rdb/6381.rdb
appendonly no
③启动三台Redis,使用命令info replication查看当前服务器主从情况
④配从(库)不配主(库)slaveof 成为某个实例的从服务器
⑤在主机上可以读写数据,在从机上写数据会报错
⑥主机挂掉,重启就行,一切如初,从机重启需重设:slaveof 127.0.0.1 6379,可以将配置增加到文件中。永久生效
从服务器挂掉后,不会重新加入master,需要重新执行命令,执行后从服务器重新复制主服务器的数据
主服务器挂掉之后小弟不会上位,重启后依然是主服务器
3)主从复制原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3D6M1QkZ-1675950071534)(E:\尚硅谷\java\尚硅谷Redis6视频课程\笔记\分析图\03-主从复制原理.png)]
4)薪火相传
配置: a slaveof b
b slaveof c
相当于主服务器是c,使用薪火相传模式
风险是一旦某个slave宕机,后面的slave都没法备份
5)反客为主
大哥挂掉后在从机执行slaveof no one 将从机变为主机
6)复制延时
由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重
13.Redis哨兵模式(反客为主自动版)
1) 是什么
反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库
2)怎么玩
①在一主二从模式下新建sentinel.conf文件,名字绝不能错
②配置哨兵,填写内容sentinel monitor mymaster 127.0.0.1 6379 1
其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。
③redis-sentinel /myredis/sentinel.conf 启动哨兵,默认端口为26379
④当主机挂掉,从机选举中产生新的主机
哪个从机会被选举为主机呢?根据优先级别:slave-priority 原主机重启后会变为从机。
优先级在redis.conf中默认:slave-priority 100(老版本Redis该配置项名有些变化),值越小优先级越高
偏移量是指获得原主机数据最全的
每个redis实例启动后都会随机生成一个40位的runid
3)Java代码连接Redis的哨兵模式
private static JedisSentinelPool jedisSentinelPool=null;
public static Jedis getJedisFromSentinel(){
if(jedisSentinelPool==null){
Set<String> sentinelSet=new HashSet<>();
sentinelSet.add("192.168.11.103:26379");//ip连接改为哨兵端口
JedisPoolConfig jedisPoolConfig =new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(10); //最大可用连接数
jedisPoolConfig.setMaxIdle(5); //最大闲置连接数
jedisPoolConfig.setMinIdle(5); //最小闲置连接数
jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待
jedisPoolConfig.setMaxWaitMillis(2000); //等待时间
jedisPoolConfig.setTestOnBorrow(true); //取连接的时候进行一下测试 ping pong
//sentinel.conf中配置的名称mymaster
jedisSentinelPool=new JedisSentinelPool("mymaster",sentinelSet,jedisPoolConfig);
return jedisSentinelPool.getResource();
}else{
return jedisSentinelPool.getResource();
}
}
14.Redis集群
1)集群可能出现的问题
主从模式,薪火相传,主机宕机,导致ip发生变化,应用程序中需要修改对应的主机地址、端口等信息
之前通过代理主机来解决,但是在Redis3.0中提供了无中心化集群配置
2)集群搭建
port 6379
pidfile "/var/run/redis_6379.pid"
dbfilename "dump6379.rdb"
dir "/home/bigdata/redis_cluster"
logfile "/home/bigdata/redis_cluster/redis_err_6379.log"
cluster-enabled yes
#设定节点配置文件名
cluster-config-file nodes-6379.conf
#设定节点失联时间,超过该毫秒,集群自动进行主从切换
cluster-node-timeout 15000
#启动Redis,确保nodes-xxxx.conf文件生成正常
#执行如下命令建立集群
cd /opt/redis-6.2.1/src
redis-cli --cluster create --cluster-replicas 1 192.168.11.101:6379 192.168.11.101:6380 192.168.11.101:6381 192.168.11.101:6389 192.168.11.101:6390 192.168.11.101:6391
#--replicas 1 采用最简单的方式配置集群,一台主机,一台从机,正好三组。
#执行后看到all 16384 slots covered表示集群搭建成功
#-c采用集群策略连接,设置数据会自动切换到相应的写主机
redis-cli -c -p 6379
#通过cluster nodes 命令查看集群信息
cluster nodes
3)集群详情
①redis cluster 如何分配这六个节点
一个集群至少要有三个主节点。
选项 --cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。
分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上。
②什么是slots
一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个
集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和
redis-cli -c –p 6379 登入后,再录入、查询键值对可以自动重定向。
不在一个slot下的键值,是不能使用mget,mset等多键操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PAb8ln3R-1675950071534)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230207213541707.png)]
可以通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中去。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oOLTKdIi-1675950071535)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230207213555540.png)]
4)集群命令
1.获取key对应的插槽值
cluster keyslot cust
2.计算插槽中有几个键
cluster countkeysinslot 12706 如果该插槽不在执行命令的服务器上,则返回0
3.返回插槽中键的数量
cluster getkeysinslot 4847 10
5)集群的故障恢复
主节点挂掉后从节点上位,主节点重连后变为从节点
如果所有某一段插槽的主从节点都宕掉,redis服务是否还能继续?
如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为yes ,那么 ,整个集群都挂掉
如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为no ,那么,该插槽数据全都不能使用,也无法存储
6)集群的Jedis开发
public class JedisClusterTest {
public static void main(String[] args) {
Set<HostAndPort>set =new HashSet<HostAndPort>();
set.add(new HostAndPort("192.168.31.211",6379));
JedisCluster jedisCluster=new JedisCluster(set);
jedisCluster.set("k1", "v1");
System.out.println(jedisCluster.get("k1"));
}
}
7)集群优势、劣势
优势:
实现扩容
分摊压力
无中心配置相对简单
不足:
多键操作是不被支持的
多键的Redis事务是不被支持的。lua脚本不被支持
由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大。