Redis
一、Nosql
- 为这么要使用Nosql:
- 因为当下数据量和访问量很大,一个机器放不下,内存不够
- 使用缓存,即可将一些数据放入缓存中,用户查询时就可以在缓存中找到数据,而不用在取数据库中查询数据
- Nosql特点:
- Redis是单线程+io多路复用,所以具有原子性(不同的操作不会被打断)
- (解耦)方便扩展
- 大数据量高性能
- 数据类型多样性,不需要设计数据库
- 传统RDBMS和Nosql
- 不支持ACID
- Nosql的四大分类:
- KV键值对:Redis
- 文档数据类型:MongoDB
- 列存储数据库
- 图关系数据库
二、Redis入门
1.安装
官网:https://redis.io/download
-
将下载文件拖入到linux中
-
解压文件:
tar -zxvf 压缩包名称
-
进入解压后的文件
cd 解压后的文件名称
-
使用命令编译(必须要有gcc 使用:gcc --version 查看)
make
-
安装
make install
-
完成,跳转到 /usr/local/bin/ 目录下即可看到redis相关文件
-
修改redis.conf配置文件
daemonize yes
-
后台启动
cd /usr/local/bin redis-server ~/redis/redis6.2.3/redis.conf
2.五大基本数据类型
0.key 操作
-
查看所有key
keys *
-
设置key - value
set key1 tom set key2 jerry set ket3 jack
-
查看是否存在key
exists key1
-
查看key的类型
type key1
-
删除key(直接删除)
del key3
-
删除key(异步删除:通知用户已经删除,实际没有,后续慢慢删除)
unlink key3
-
设置key的有效期(单位:s)
expire key1 10
-
查看key的有效期(返回剩余时间;过期时返回-2;永不过期:-1)
ttl key1
-
切换库(默认0)
select 1
-
查看库中的key的数量
dbsize
-
清空当前库
flushdb
1.字符串 String
String类型可以存放任何数据,最多存放512M
-
设置 key - value
# 1.直接添加,如果不存在key则添加key,如果已存在key则替换value set key value # 如: set k1 v1000 # 2.当key不存在时才能添加 setnx key value setnx k1 v2000 # 失败 setnx k2 v2000 # 成功
-
获取值
get key # 如: get k1
-
字符串追加
append key value # 如: append k1 111
-
获取长度
strlen key # 如: strlen k1
-
对数字值的 +1 或 -1
# +1 incr key # -1 decr key
-
对数字值的 +任意值 或 -任意值
# 加 incrby key number # 如:+10 incrby k3 10 # 减 decrby key number # -50 decrby k3 50
-
同时设置多个 key - value
mset key value key value ... msetnx key value key value # 只能当key不存在时才能设置
-
同时得到多个value
mget key key key
-
根据范围设置或取值
set name jerry getrange name 0 3 setrange name 1 --- # 得到j---y,依次向后覆盖
-
在设置值的时候添加过期使劲按
setex key second value # 如下: setex gender 20 man
-
取旧值,设新值
getset key value # 如: getset name tom #返回的是原来的值,但是该key的值已经变为tom
2.列表 List
list是一个双向链表(有序)
-
lpush:从左边加入
lpush list1 v1 v2 v3 v4
取出时, 显示顺序是:v4 v3 v2 v1;因为是从左边添加,添加时,所有的value会被向右移一个位置。
-
rpush:从右边加入
rpush list2 v1 v2 v3 v4
取出时, 显示顺序是:v1 v2 v3 v4;因为从右边添加,添加时,所有的value会被向左移一个位置。
-
lpop:从左边弹出一个值
lpop list1
-
rpop:从右边弹出一个值
rpop list1
-
rpoplpush:弹出一个key的右边的值加入到另一个key的左边
rpoplpush key1 key2
表示从key1的右边弹出一个值,加入到key2的左边
-
lrange:取值
range key 0 -1 # 表示取全部的值
-
lindex:按下标取值
lindex key 0 # 取下标为0的值
-
llen:列表长度
llen key
-
linsert:在某一个值的前面或后面添加新值
linsert key before/after value newvalue
-
lrem:删除前面的几个value(从左往右删除)
lrem key 2 value # 如: lrem list1 2 v1 # 表示删除前面的量v1的值(不足则全部删除)
-
lset:替换
lset key index value # 如: lset list1 1 v100 #表示将下标为1的值替换为v100
底层原理:
Redis底层使用QuickList,由ziplist组成。当只有少量数据的时候,会申请一段连续的内粗空间,为ziplist。随着数据量的增加,会将多个ziplist使用链表指针连接起来组成quicklist。因为双向链表的指针会占用大量的空间。使用quickList会节省大量的空间。
3.集合 Set
即无序、不重复。底层是一个value为null的hash表。
-
sadd:添加一个或多个值
sadd key value value value ... # 重复的值会被自动排除
-
smembers:取出set中的全部值
smembers key
-
sismember:判断set中是否有某个值,有返回1,没有返回0
sismember key value
-
scard:返回集合的元素个数
scard key
-
srem:删除集合中的一个或几个元素
srem key value value .... # 删除一个或几个值
-
spop:随机从集合中弹出一个值
spop key count # 随机弹出count个值
-
srandmember:随机从集合中取个n值,不会删除
srandmember key count # 表示从一个集合中随机取count个值
-
smove:把集合中的一个值移动到一个另一个集合中
smove key1 key2 value # 把key1中的value移动到key2中
-
sinter:返回两个集合的交集
sinter key1 key2
-
sunion:返回两个集合的并集
sunion key1 key2
-
sdiff:返回两个集合中的差集(key1 中有,key2中没有的值 )
sdiff key1 key1
底层结构:
与java中HashSet与HashMap的关系类似
4.哈希 Hash
hash中的values使用key-value的映射
-
hset:设置值
hset key field value # 如:添加key为user field为id value为1 hset user id 1
-
hget:取值
hget key field # 如:获取id hget user id
-
hmset:批量设置值
hmset key field value field value # 如: hmset user2 id 2 name lisi age 20
-
hexists:判断key中是否有field
hexists key field # 如:判断user2中是否有name hexists user2 name
-
hkeys:列出该key中所有的filed
hkeys key
-
hvals:列出该key中所有value
hvals key
-
hincrby:为key中的指定filed +1
hincrby key field increment # 如:年龄+5 hincrby user2 age 5
-
hsetnx:为key中添加field-value(只有当该field不存在时)
hsetnx key field value
5.有序集合Zset
Zset和set类似,都会自动排重;不同的是,Zset的value具有评分,可以根据评分来排序
-
zadd:添加
zadd key score value score value ... # 如: zadd key1 8 z1 9 z2 10 z3
-
zrange:查询value
zrange key start stop # 如:显示全部 zrange key1 0 -1 # 显示评分 zrange key1 0 -1 withscores # 根据评分的范围显示(5 - 10) zrangebyscore key1 5 10 # 根据评分范围反向显示 zrevrangebyscore key1 10 0
-
zincrby:更改评分
zincrby key increment value
-
zrem:删除元素
zrem key value
-
zcount:统计评分范围内的元素个数
zcount key min max
-
zrank:查看元素在集合中的排名(从0开始)
zrank key value
底层原理:
使用hash(方便存储score和value)+跳跃表(快速查找)实现
3.配置文件
4.发布订阅
即一种通信模式,消息的订阅者可以收到消息发布者发布的消息。
Redis可以订阅任意频道的消息。
实现:
客户端1:
# 订阅
subscribe channel1
客户端2:
# 发布
publish channel1 hello
当客户端2通过channel1发布消息后,客户端1会收到该消息。
5.Redis6中的新数据类型
1.Bitmaps
-
setbit:根据偏移量添加
setbit key offset value
-
getbit:根据偏移量,获取该位置是否置为1
getvit key offset
-
bitcount:统计置为1的位数个数
bitcount key [start,end]
-
bitop:取并集(and)、交集(or)
2.HyperLogLog
用于统计不同元素的基数(占用空间小)
可以做到去重
-
pfadd:添加基数
pfadd key value value value ...
-
pfcount:统计基数个数
pfcount key
-
pfmerge:将两个HyperLogLog的value复制到一个HyperLogLog中
# 将key1、key2中的value添加到key3中 pfmerge key3 key1 key2
3.Geospatial
即地理坐标,用于存放二维坐标
-
geoadd:添加
geoadd key longitude latitude member # 如:添加上海和重庆的坐标 geoadd china:city 121 31 "shanghai" 106 29 chongqing
-
geopos:获取member的坐标
geopos key member # 如:获取上海的坐标 geopos china:city shanghai
-
geodist:获取d两地的直线距离
geodist key member1 member2 # 如:获取上海与重庆的直线距离 geodist china:city shanghai chongqing
-
georadius:获取以某个点为中心,半径内的点
georadius key longitude latitude radius m|km # 如:获取以 110 30 为中心,半径1000km的范围内的点 georadius china:city 110 30 1000 km
6.Jedis操作
类似于JDBC,可以使用java连接redis
注意问题
-
redis.conf配置文件的设置
1.注释如下配置,否则只能本地访问(linux内部)
#bind 127.0.0.1 -::1
2.修改模式 yes改为no
protected-mode no
3.远程连接无法访问,需要关闭linux防火墙
systemctl stop firewalld
1.Redis连接测试
导入gav:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
java代码:
public class demo01 {
public static void main(String[] args) {
Jedis jedis = new Jedis("49.234.85.106",6379);
String ping = jedis.ping();
System.out.println(ping);
}
}
// 输出:
// PONG
2.数据命令的简单操作
@Test
public void test1(){
Jedis jedis = new Jedis("192.168.1.118",6379);
/*==========String==============*/
jedis.set("name", "zhangsan");
String name = jedis.get("name");
System.out.println(name);
jedis.append("name", "ni hao");
String name1 = jedis.get("name");
System.out.println(name1);
Boolean exists = jedis.exists("name");
System.out.println(exists);
/*=============set==========*/
jedis.sadd("s1", "v1","v2","v3");
Set<String> s1 = jedis.smembers("s1");
for (String s : s1){
System.out.print(s + " ");
}
System.out.println();
/*=============list===========*/
jedis.lpush("list1", "v1","v2","v3");
for(int i=0; i<=jedis.llen("list1"); i++){
System.out.print(jedis.lpop("list1") + " ");
}
System.out.println();
}
3.简单场景使用
场景:手机验证码
- 手机验证码6位有效期为120s
- 每个手机每天只能申请3次
- 成功返回成功,失败返回失败
环境:springboot
业务类:
@Service
public class util {
// 随机生成6为验证码
public String getPhoneCode(){
Random random = new Random();
StringBuilder str = new StringBuilder();
for(int i=0; i<6; i++){
str.append(random.nextInt(10));
}
System.out.println(str.toString());
return str.toString();
}
// 发送验证码
public boolean connectRedis(String phone, String code){
// 连接redis
Jedis jedis = new Jedis("192.168.1.118",6379);
// 约定每个手机号每天的发送次数的key
String countKey = "countKey:"+phone;
// 约定验证码的key
String codeKey = "codeKey:"+phone;
// 获取发送次数
String s = jedis.get(countKey);
if(s == null){ // 第一次发送
jedis.setex(countKey, 60*60*24,"1");
jedis.setex(codeKey,120,code);
}else if(Integer.valueOf(s) < 3){ // 不超过三次
jedis.incr(countKey);
jedis.setex(codeKey,120,code);
}else{ // 超过三次
System.out.println("超过三次了");
return false;
}
jedis.close();
return true;
}
// 验证
public boolean verification(String phone,String code){
// 连接redis
Jedis jedis = new Jedis("192.168.1.118",6379);
// 约定验证码的key
String codeKey = "codeKey:"+phone;
// 获取验证码信息
String s = jedis.get(codeKey);
System.out.println(s);
try{
if(s == null){
return false;
}
if(s.equals(code)){
return true;
}
return false;
}finally {
jedis.close();
}
}
}
contoller:
@RestController
public class PhoneCodeController {
@Autowired
private util util;
@GetMapping("/get/code/{phoneNumber}")
public String getCode(@PathVariable(value = "phoneNumber")String phoneNumber){
String code = util.getPhoneCode();
boolean b = util.connectRedis(phoneNumber, code);
if(b){
return code;
}else{
return "尝试次数太多,明天再来吧~";
}
}
@PostMapping("/post/code/{phoneNumber}/{code}")
public String postCode(
@PathVariable(value = "phoneNumber")String phoneNumber,
@PathVariable(value = "code")String code){
boolean verification = util.verification(phoneNumber, code);
if(verification){
return "验证成功!";
}else{
return "验证失败~";
}
}
}
获取验证码:
验证:
4.springboot整合Redis
在第三步中使用的是jedis直接连接,下面将使用springboot整合redis
1.依赖
<!--redis相关-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.10.RELEASE</version>
</dependency>
<!--集成redis-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.2</version>
</dependency>
2.yml配置文件
spring:
redis:
# redis 服务器地址
host: 192.168.1.118
# 服务器端口号
port: 6379
# 数据库索引(默认为0)
database: 0
# 连接超时时间
timeout: 1800000
password: 123456
lettuce:
pool:
# 连接池最大连接数,负值表示没有限制
max-active: 20
# 最大空间连接
max-idle: 5
# 最小空闲连接
min-idle: 0
# 最大阻塞等待时间
max-wait: -1
3.自定义RedisTemplate
如果使用默认的redisTemplate,则在添加key或value时不能被序列化,所以需要自定义
@Configuration
public class RedisConfig {
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 设置key的序列化方式:字符串的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
// 设置value的序列化方式:json
Jackson2JsonRedisSerializer redisSerializer = new Jackson2JsonRedisSerializer(Object.class);
template.setValueSerializer(redisSerializer);
template.setHashValueSerializer(redisSerializer);
template.afterPropertiesSet();
return template;
}
}
4.测试
@RestController
public class RedisDemo1 {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/get/redis")
public String getRedis(){
redisTemplate.opsForValue().set("test", "测试");
Object test = redisTemplate.opsForValue().get("test");
return (String)test;
}
}
此时,能够得到value且放入redis中后不会被转义
在使用时,可以使用util工具类
7.事务
1.事务概述
Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:
- 批量操作在发送 EXEC 命令前被放入队列缓存。
- 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
一个事务从开始到执行会经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
过程示意图:
失败的两种情况:
一、组队时错误
当在组队阶段有错误时,全部命令都会失败
二、执行时错误
当在执行命令时出错,只会时该条命令失败,其他不受影响
2.事务冲突
在实际中,存在事务的冲突问题:
例如:一个账户,在三个设备”同时“转出金额,但是转出之和大于余额,就会导致事务冲突。
在redis中,使用锁的机制来解决这些问题。
1.悲观锁
2.乐观锁
使用wathc
命令,实现乐观锁
客户端1:
127.0.0.1:6379> set name "zhangsan"
OK
127.0.0.1:6379> watch name
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set name "lisi"
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
127.0.0.1:6379> get name
"lisi"
127.0.0.1:6379>
客户端2:
127.0.0.1:6379> get name
"zhangsan"
127.0.0.1:6379> watch name
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set name "wangwu"
QUEUED
127.0.0.1:6379(TX)> exec
(nil)
127.0.0.1:6379> get name
"lisi"
127.0.0.1:6379>
3.事务使用案例
背景:秒杀系统,在高并发的情况下,会出先数据错误,如下:
业务逻辑:
某商品实行秒杀促销,库存有10个;数据库中有两个数据:proNum:user:proid
表示库存,user:proid
表示抢到的用户信息。
@Service
public class Redis02Service {
@Resource(name = "redisTemplate1")
private RedisTemplate redisTemplate;
public boolean doSecKill(String userId, String proId){
// 1.判断userId、ProId是否为空
if(userId == null || proId == null){
System.out.println("用户错误~");
return false;
}
// 2.拼接key
String proNumKey = "proNum:" + proId;
String userKey = "user:" + proId;
// 3.判断proId是否正确、查询库存
Integer proNum = (Integer)redisTemplate.opsForValue().get(proNumKey);
if (proNum == null){
System.out.println("秒杀不存在~");
return false;
}
if(Integer.valueOf(proNum) < 1 ){
System.out.println("来晚了,结束了~");
return false;
}
// 4.判断是否重复秒杀
Boolean member = redisTemplate.opsForSet().isMember(userKey, userId);
if (member){
System.out.println("已近抢过了,下次再来~");
return false;
}
// 5.秒杀
// 库存-1
redisTemplate.opsForValue().decrement(proNumKey);
// 添加用户列表
redisTemplate.opsForSet().add(userKey,userId);
System.out.println("秒杀成功!");
return true;
}
}
controller:
为方便实现高并发,用户id采用随机方式。
@RestController
public class RedisController {
@Autowired
private Redis02Service redis02Service;
// @GetMapping("/seckill/{userId}/{proId}")
@GetMapping("/seckill")
public String secKill(
// @PathVariable(value = "userId")String userId,
// @PathVariable(value = "proId")String proId
){
int userId = new Random().nextInt(50000);
boolean isSuccess = redis02Service.doSecKill(String.valueOf(userId), "1");
if(isSuccess){
return "秒杀成功!";
}
return "秒杀失败";
}
}
高并发:使用JMETER实现每秒钟2000个请求。
结果:
秒杀过程出现错误:顺序完全出错。
库存为负数,超卖现象。
解决:使用事务。
这里使用的是jedis(RedisTemplate不会。。)
修改后的业务代码:
@Service
public class Redis02Service2 {
public boolean doSecKill(String userId, String proId){
// 1.判断userId、ProId是否为空
if(userId == null || proId == null){
System.out.println("用户错误~");
return false;
}
// 2.拼接key
String proNumKey = "proNum:" + proId;
String userKey = "user:" + proId;
// 连接redis
Jedis jedis = new Jedis("192.168.1.119", 6379);
// 3.判断proId是否正确、查询库存
String proNum = jedis.get(proNumKey);
if (proNum == null){
System.out.println("秒杀不存在~");
jedis.close();
return false;
}
if(Integer.valueOf(proNum) <= 0 ){
System.out.println("来晚了,结束了~");
jedis.close();
return false;
}
// 监视库存
jedis.watch(proNumKey);
// 4.判断是否重复秒杀
Boolean sismember = jedis.sismember(userKey, userId);
if (sismember){
System.out.println("已近抢过了,下次再来~");
jedis.close();
return false;
}
// 开启事务
Transaction multi = jedis.multi();
// 命令入队
multi.decr(proNumKey);
multi.sadd(userKey,userId);
// 事务提交
List<Object> exec = multi.exec();
// 如果返回的为空,或者操作过成功的命令为0,则表示做错失败
if(exec != null && exec.size()>0){
System.out.println("秒杀成功!");
jedis.close();
return true;
}
return false;
}
}
4.库存遗留问题
由于乐观锁的机制,版本号的频繁改变,所以会导致控制台显示抢完时,实际数据库中还有剩余。
使用LUA脚本
解决
8.持久化操作
持久化:即,将数据保存到磁盘。
1.RDB
机制:每隔一段时间,将数据集快照持久化到磁盘。
redis会创建一个子进程(fork)来进行持久化操作。持久化时,先将数据写入到临时文件,再将持久化好的临时文件替换上次持久化好的持久化文件。写时复制技术
适用于大规模数据恢复。
缺点:最后一次持久化的数据可能会丢失。占用两倍的空间所以适用于对数据恢复完整性要求不高的环境,否则应该使用AOF。原因如下:
配置文件解读:redis.conf
1.持久化规则:
- 一小时内至少有一个key改变
- 5分钟内至少有100个key改变
- 1分钟内至少有10000个key改变
三者满足其一即持久化
374 # Unless specified otherwise, by default Redis will save the DB:
375 # * After 3600 seconds (an hour) if at least 1 key changed
376 # * After 300 seconds (5 minutes) if at least 100 keys changed
377 # * After 60 seconds if at least 10000 keys changed
378 #
379 # You can set these explicitly by uncommenting the three following lines.
380 #
381 # save 3600 1
382 # save 300 100
383 # save 60 10000
2.持久化文件名
默认持久化名称为dump.rdb
430 # The filename where to dump the DB
431 dbfilename dump.rdb
3.持久化文件位置
即:保存在当前目录下(在哪个目录下启动redis,就在哪个目录下生成持久文件)
451 # The Append Only File will also be created inside this directory.
452 #
453 # Note that you must specify a directory here, not a file name.
454 dir ./
4.当磁盘已满时,关闭redis写操作
stop-writes-on-bgsave-error yes
5.检查完整性
rdbchecksum yes
RDB恢复操作:RDB恢复会根据持久化的文件进行自动恢复,但可能会丢失最后一次的持久化数据(如上所述)
2.AOF
AOF以日志的形式来记录每个写操作(增量保存),记录所有写操作(增、删、改),只追加文件,不修改文件。Redis启动时就会读取该文件以恢复数据。(会记录命令)
AOF默认不开启。
开启:
appendonly yes # 开启AOF
# The name of the append only file (default: "appendonly.aof")
# 持久化文件名称
appendfilename "appendonly.aof"
RDB生成的文件在哪,AOF生成的文件也在哪
AOD/RDB同时开启,默认使用aof的数据
数据恢复:与RDB一样,AOF启动数据库时,也对读取响应的文件以恢复数据。
异常修复:当appendonly.aof文件损坏(异常)时,可以对其进行修复。
如下:
1.在文件添加错误信息:
2.启动redis:
3.修复文件:使用redis-check-aof命令
4.查看文件:文件中手动添加的内容被清除,redis启动成功且数据恢复正常。
AOF同步频率设置:
redis.conf配置文件中:
# appendfsync always
appendfsync everysec
# appendfsync no
- 总是同步:即每次写入操作都会持久化记录(数据完成性高,性能差)
- 每秒钟同步一次
- 从不主动同步,将同步时机交给操作系统
Rewrite压缩:
重写压缩操作:对于多条命令,AOF最终都会将他压缩为尽可能少的命令(只关注结果,不关注过程
),以减少日志文件的大小,如下:
set k1 v1
set k2 v2
# 压缩为
set k1 v1 k2 v2
当文件达到一定大小时,才会进行重写压缩机制
也是使用fork来实现(写时复制技术)
3.比较
- 推荐两者同时启用
- 如果对数据的完整性要求不高,可单独使用RDB
- 如果只做缓存,两者都不使用
9.主从复制
主机数据更新后根据配置和策略,自动同步到备用机的master/slaver机制。Madter以写为主,slaver以读为主。
优点:
- 读写分离(主机做读操作,备用机做写操作)
- 快速的容灾恢复(从机宕机后,可以快速切换到其他的备用机)
1.配置
1.创建配置文件
内容如下(三个文件都是这样,只需修改端口号即可):
include ~/redis/MyRedis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb
2.分别根据这三个配置文件启动redis
启动命令:redis-cli -p port
3.查看当前的角色
使用命令:info replication
由于当前没有任何配置,所以三台都是主机(master)
4.设置从机
使用命令:slaveof 主机ip 主机端口号
再次查看角色:
此时6380已经作为6379的从机了。
测试:
在6379(主机)中写入数据,在6380、6381(从集中可以读到该数据)
注意:
1.主机可以进行读、写操作
2.从机不能进行写操作
2.主从机宕机问题
1.从机断开连接重连
环境:一台主机两台从机。
当一台从机宕机后(shutdown),再重连有以下注意点:
- 一台从机宕机后,主机中显示只剩一台从机
- 宕机的服务重启后,是作为一台单独的主机(不会自动加入到主服务器的从机中)
- 使用命令重新连接后,会自动复制主机中的全部数据(即使在断开连接过程中,主机有添加新的数据)
2.主机断开连接及重连
环境:一台主机两台从机
当主机宕机、重连后,有以下几点:
- 主机断开连接后,从机不做任何变化(从机的主机还是原来的主机,但是将主机的状态显示为掉线状态)
- 主机重连后,依旧是主机,具有原来的从机;从机将自己的主机状态改为在线
3.薪火相传
从服务器可以把其他的从服务器作为自己的主服务器
缺点:当从服务1宕机时,其下面的从服务器也都无法操作。
命令:
- 6380:slaveof 127.0.0.1 6379
- 6381:slaveof 127.0.0.1 6380
此时,6381的 info replication
显示6380为其主服务器
4.反客为主
当主服务器宕机后,从服务器会自动晋升为主服务器。
使用命令:slaveof no one
缺点:需要手动输入命令才能完成
5.主从复制原理
- 当从服务器连接到主服务器后,会主动发送一个同步数据请求
- 主服务器接收到同步请求后会将自己的数据进行持久化,然后将持久化文件(dump.rdb)发送给从服务器,从而实现同步
- 当主服务进行写操作后,会将新的数据发送给从服务器,以保证数据的同步。
6.哨兵模式
哨兵模式是反客为主的自动版,能够后台监控主机是否出现故障,如果出故障了,将根据投票数自动将从服务器转换为主服务器。
配置:
-
准备配置文件:
sentinel.conf
sentinel monitor mymaster 127.0.0.1 6380 1
测试
-
依次启动:master >>> slave(主服务器都为6379) >>> sentinel
# sentinel 启动命令 redis-sentinel ~/redis/MyRedis/sentinel.conf
即可看到哨兵配置完成,监视对象为6379,从机为6380、8381
-
关闭主服务器(6379)
哨兵显示:(将主服务器切换为6380)
6380状态:(6380变为主服务器,且有从服务器一个6381)
-
启动6379:
6379变为6380的从服务器:
查看sentinel.conf配置文件:(主服务器变为6380)
选举规则:
-
选择优先级靠前的
在配置文件中有:(值越小,优先级越高)
replica-priority 100
-
选择偏移量大的(与原主服务器同步程度最高的从服务器)
-
选择runid最小的从服务(redis实例启动后,会随机产生一个40位的runid)
7.java中的主从复制
10.集群
Redis中的集群采用无中心化集群
:即不使用代理服务器入口;而是任何一个模块的主服务器都可以作为入口。根据服务的类型判断该操作是否属于当前模块的功能操作,如果不是就会将该搞作转移给其他的主服务器操作。
集群的优点:
- 水平扩大redis容量
- 分摊压力
- 无中心配置相对简单
集群的缺点:
- 多建操作不被支持
- 多建的Redis事务不被支持;lua脚本不支持
1.集群配置
-
环境准备(本地模拟六台服务器):6379、6380、6381、6389、6390、6391
-
配置文件内容:(其他的类似)
include /root/redis/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
-
启动全部服务器:(启动并生成集群配置文件)
-
开启集群:
跳转到redis安装目录的src文件夹下:
cd ~/redis/redis-6.2.3/src
使用命令:
./redis-cli --cluster create --cluster-replicas 1 192.168.1.119:6379 192.168.1.119:6380 192.168.1.119:6381 192.168.1.119:6389 192.168.1.119:6390 192.168.1.119:6391
其中:1表示最简单的集群方式(3主3从);IP必须使用真实IP
输入yes表示接受该分配方式。
-
以集群方式连接:
./redis-cli -c -p 6379
-
查看集群信息:
cluster nodes
2.分配规则
- 一个居群至少有三台主机(master)
- 尽量保证每个主机都不在同一个IP地址
- 尽量保证每个主机与其从机不再统一个IP
这样确保一个IP的失效,其他的还能工作。
slots(插槽):
在启动集群后,会显示 16384 slots :表示该集群一共有16384个插槽,每一个插槽可对应一个key。
在存入数据时,根据key计算(CRC(key)%16384)其属于哪个插槽。
如上集群:
- 主机6379:0-5460
- 主机6380:5461-10922
- 主机6381:10923-16383
如:添加一个数据,显示下面的信息
多key-value添加:
由于多数据同时添加,无法计算对应的插槽值,所以需要使用组
来添加:
mset key1{组名} value key2{组名} value key3{组名} value
其他操作:
-
查询key对应的slot值:
cluster keyslot String
-
查询某个插槽内的key的数量:
cluster countkeysinslot slot
只能查看自己插槽范围内的
-
查询某个插槽内的key:
cluster getkeysinslot slot max
故障恢复:
当一台主机宕机后,其从机(slave)会自动变为主机(master),而当原来的主机重新启动后,就会变为新的主机的从机。注意:需要超时15秒
测试:关闭主机6379(其从机为6391)
看到6391变为主机,6379关闭。
启动6379:
看到6379变为6391的从机。
主从都宕机:
如果某一块的主机和从机都宕机,有如下两种情况:
在redis.conf配置文件中,有如下配置:
cluster-require-full-coverage yes
- 如果是yes,则表示当一组主从宕机后,全部集群都不再工作
- 如果是no,则表示当一组主从宕机后,则该组不再工作
3.java操作Redis集群
public class demo1 {
public static void main(String[] args) {
HostAndPort hostAndPort = new HostAndPort("192.168.1.119", 6391);
JedisCluster jedisCluster = new JedisCluster(hostAndPort);
jedisCluster.set("test", "cluster-java");
String test = jedisCluster.get("test");
System.out.println(test);
}
}
需要注意的是:如果当前使用的IP+端口的服务关闭了,则会调用失败;所以可以在JedisCluster中传入一个Set(包含集群中的多个服务器地址)
上面的方法是直接使用Jedis,下面是使用springboot+redistemplate
:
只需要在配置文件中配置:
spring:
redis:
# redis 服务器地址
host: 192.168.1.119
# 服务器端口号
port: 6379
# 数据库索引(默认为0)
database: 0
# 连接超时时间
timeout: 1800000
lettuce:
pool:
# 连接池最大连接数,负值表示没有限制
max-active: 20
# 最大空间连接
max-idle: 5
# 最小空闲连接
min-idle: 0
# 最大阻塞等待时间
max-wait: -1
# 集群配置
cluster:
nodes: 192.168.1.119:6379,192.168.1.119:6380,192.168.1.119:6381,192.168.1.119:6389,192.168.1.119:6390,192.168.1.119:6391
配置完成后即可直接使用redisTemplate访问
11.应用问题
1.缓存穿透
简单理解:当Redis作为缓存时,在正常情况下,用户访问web服务器请求数据可以从redis缓存中直接获取,少量缓存中没有的数据在向数据库(Mysql等)中查询。但是,当访问量突然增大,Redis的命中率降低(用户的请求数据不能从缓存中直接获取),就需要大量的访问数据库,就有可能导致数据库崩溃,而此时Redis依然正常运行。即缓存穿透。
可能造成的行为:
- Redis无法查询到数据
- 大量的非正常访问
解决方案:
- **对空值缓存:**当一个查询的结果为空时,任然将其添加到缓存(null),但是该空结果的过期时间会很短,最长5分钟。
- **设置可访问的名单(白名单):**使用bitmaps类型数据定义一个可访问名单,名单id作为bitmaps的偏移量,每次访问和bitmaps里面的id进行比较,只有在里面的id才能访问。缺点:每次都要查询bitmps,效率低。
- **使用布隆过滤:**其底层原理和bitmaps类似,但是对其进行了优化,便于快速查询。缺点:命中率低。
- **进行实时监控:**当发现Redis的命中率急剧下降时,需要排查访问对象和访问的数据,可以设置黑名单对其限制。
2.缓存击穿
简单理解:当Redis中没有出现大量key过期且Redis运行正常,而数据库的访问压力瞬间增大,则会造成缓存击穿。
可能造成的行为:
- 由于redis中的某个热门数据过期而导致数据库访问量大大增加。
解决:
- 预先设置热门数据:在访问高峰之前,把热门数据加入到redis中,并加大时长。
- 实时调整:现场监控哪些数据时热门的,适时调整其key的过期时长。
- 使用锁:当redis查询到的结果为空时,将其锁起来,不允许访问;过一段时间再访问,如果返回不为空了,就将其解锁。缺点:效率低下。
缓存穿透与缓存击穿的区别:
- 首先:缓存穿透是由于Redis中本身就不存在某个key而导致数据库访问量大大增加;而缓存击穿是由于Redis中原本有该key,而现在这个key过期了导致的。
- 其次是造成原因:缓存穿透较大可能是由于黑客攻击;而缓存击穿是由于key的过期所导致。
3.缓存雪崩
简单理解:缓存雪崩是由于在短期内,由于大量的key过期(缓存失效),导致服务器大量的访问数据库导(底层压力骤加),致服务器瘫痪,造成雪崩。
解决:
- **构建多级缓存架构:**nginx+redis+其他缓存
- **使用锁或队列:**有类似于阻塞的情况,效率低,比适用于高并发场景。
- **设置过期标志更新缓存:**记录缓存数据是否过期或将要过期,通知其他线程在后台更新过期时间。
- **将缓存失效时间分散开:**即错开每个可以的过期时间。
4.分布式锁
在分布式(或集群)中,由于每个服务或服务器所在的地址不同,所以在使用锁得时候,只对单体有效,而其他的服务中不受此锁的约束,所以需要解决这个问题,让一把锁在全局能够有效。
锁应具有的特性:
- 互斥性:任何时刻,只能有一个客户端能有拥有锁
- 没有死锁:即保证一个客户端在没有主动解锁的情况下也能够自动解锁,保证其他客户端能够使用(设置过期时间)
- 加锁和解锁的必须时同一个客户端
- 加锁和解锁必须具有原子性
实现方式:
- 基于数据库实现分布式锁(alibaba的seata在在数据库底层使用了行锁,来控制全局事务,可以借鉴其思想)
- 基于Redis(性能高)
- 基于Zookeeper(用于分布式,常作为服务注册中心,可靠性最高)
在Redis中的实现:
**1.**使用命令setnx key value
即为该数据上锁
使用命令del key
即为该数据解锁
解释:当时用setnx后,如果该key存在,则不能再添加该key的数据,只有当删除这个key之后才能添加。
缺点:必须要先删除才能添加
**2.**为解决上面的问题,可以为该数据添加过期时间expire key seconds
当过期时间到了之后,就会自动解锁,即可添加新的数据。
缺点:如果再添加值后突然出现异常,导致没有设置过期时间则无法解锁。
**3.**为解决2的问题,可使用如下命令完成添加数据的同时加锁
set key value nx ex seconds
nx:表示加锁;ex:表示设定过期时间
java代码演示:
@RestController
public class LockController {
@Resource(name = "redisTemplate1")
private RedisTemplate redisTemplate;
@GetMapping("/test/lock")
public String testLock(){
// 上锁,即setnx命令
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "lock");
// 使用 set key value nx ex seconds 命令
// Boolean lock1 = redisTemplate.opsForValue().setIfAbsent("lock", "lock",1, TimeUnit.SECONDS);
// 当上锁成功(表明之前时解锁状态)
if (lock){
// 获取值
Object val = redisTemplate.opsForValue().get("number");
// 判断是否为空
if(val == null){
System.out.println("key不存在");
return "key不存在";
}
// +1并存入
int num = Integer.valueOf(String.valueOf(val));
redisTemplate.opsForValue().set("number", ++num);
// 解锁
redisTemplate.delete("lock");
return "success";
}
// 上锁失败(当前锁被其他使用)
return "is locking";
}
}
上述代码演示了通过setnx命令来实现上锁机制,再实际使用中,使用同一个锁(key名称相同)即可实现分布式锁的全局性
上述代码依然存在为题:
@RestController
public class LockController {
@Resource(name = "redisTemplate1")
private RedisTemplate redisTemplate;
@GetMapping("/test/lock")
public String testLock(){
// 使用UUID作为lock的值
String uuid = UUID.randomUUID().toString();
// 上锁
// 使用 set key value nx ex seconds 命令
Boolean lock1 = redisTemplate.opsForValue().setIfAbsent("lock", "lock",1, TimeUnit.SECONDS);
// 当上锁成功(表明之前时解锁状态)
if (lock){
// 获取值
Object val = redisTemplate.opsForValue().get("number");
// 判断是否为空
if(val == null){
System.out.println("key不存在");
return "key不存在";
}
// +1并存入
int num = Integer.valueOf(String.valueOf(val));
redisTemplate.opsForValue().set("number", ++num);
// 解锁
// 在UUID相同的时候才能解锁
Object uuidFromRedis = redisTemplate.opsForValue().get("lock");
if(uuid.equals(String.valueOf(uuidFromRedis))){
redisTemplate.delete("lock");
}
return "success";
}
// 上锁失败(当前锁被其他使用)
return "is locking";
}
}
还有问题:
缺乏原子性。
- 在A操作的过程中,到了正要删除锁的时候(可以极端的理解为:uuid已近比较相等,但还没有执行删除这个命令),由于过期时间到了,自动释放了锁;
- 此时B操作就可以获得锁,并操作;
- 而后,A操作就释放了B的锁
- 需要结合代码理解(这是在很短的一段时间内)
解决:
使用lua脚本,该脚本时嵌入式脚本,不允许被打断,所以保证了原子性。
12.Redis6新功能
1.ACL 访问控制列表
官网:https://redis.io/commands
-
查看全部用户列表
acl list
-
查看当前用户
acl whoami
-
查看命令集集
acl cat # 查看具体命令 acl cat string acl cat list
-
创建用户
acl setuser 用户名 on >密码 ~* +@all"
-
切换用户
auth 用户名 密码
2.IO多线程
Redis6中依然是单线程+IO多路复用
这里的多线程只是redis对网络数据和协议处理的多线程,而执行命令依然是单线程。
3.cluster工具
在redis6中,集成了Ruby环境,不需要再安装该环境。