redis

Redis

36、小结及拓展_哔哩哔哩_bilibili

1.redis概念

Redis (全称:Remote Dictionary Server 远程字典服务)是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它是一个运行在内存中的数据结构存储系统,它可以用作数据库、缓存消息中间件

redis可以用来干嘛

  1. 内存存储、持久化、内存中的内容是断电即失、所以说持久化很重要(rdb,aof)
  2. 效率高,可以用来高速缓存
  3. 发布订阅系统
  4. 地图信息分析
  5. 计时器,计数器(浏览量!)

特性

多样的数据类型,集群持久化,事务

2、windows上下载redis

官方不建议在windows上使用redis,很久没有更新维护了,可以在github上去下载redis

下载完成解压之后,双击redis-server.exe开启redis服务(注意redis端口是6379)

image-20230802142557205

测试redis

开启redis-cli.exe开启redis客户端执行ping指令显示pong表示已经开启完成

3、Linux安装redis

我使用的是虚拟机ubuntu

1、首先我们需要将下载的redis.tar.gz文件保存到linux上

找到redis的安装目录

cd /redis相应的安装目录

2、将redis.tar.gz文件移到/opt中

mv redis-7.0.12.tar.gz /opt

3、将文件进行解压

tar -zxvf redis-7.0.12.tar.gz

4、进入到解压之后的文件目录

cd redis-7.0.12/

5、安装需要编译的命名

yum install gcc-c++ # centos
apt-get install gcc-c++ # ubuntu
make # 安装依赖
make install # 安装依赖

注意:这是centos升级gcc -C++的操作

安装redis6.0以上版本需要升级gcc到5.3及以上,如下:升级到gcc 9.3
yum -y install centos-release-scl
yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils scl enable devtoolset-9 bash
需要注意的是scl命令启用只是临时的,退出shell或重启就会恢复原系统gcc版本。
如果要长期使用gcc 9.3的话:
echo source /opt/rh/devtoolset-9/enable /etc/profile
这样退出shell重新打开就是新版的gcc了

ubuntu操作

输入命令行:
sudo apt-get install gcc-9
执行完毕后再输入:
sudo apt-get install g++-9
接着进入/usr/bin目录下删除旧版本gcc/g++文件:
cd /usr/bin
sudo rm gcc g++
最后再将gcc/g++和新安装的gcc-9/g+±9关联起来:
sudo ln -s gcc-9 gcc
sudo ln -s g++-9 g++
此时查看gcc版本:
gcc -V
————————————————
版权声明:本文为CSDN博主「我宿孤栈」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_37346140/article/details/127686966

6、进入到redis的默认安装路径 usr/local/bin

cd usr/local/bin/

7、将redis.conf文件拷贝到当前文件

mkdir redisconfig # 创建一个目录
cp /opt/redis-7.0.12/redis.conf redisconfig

我们以后就使用这个文件,可以保护原来的文件,当出现错误可以恢复

8、将redis改为后台运行

vim redis.conf
# 或者使用vi命令直接修改
vi redis.conf daemonize yes

9、启动redis服务

注意我们时使用指定的配置文件启动

redis-server redisconfig/redis.conf

10、启动redis-cli客户端测试连接

redis-cli -p 6379

使用ping测试连接

11、11.查看redis进程是否开启

ps -ef|grep redis # 过滤redis的进程

12.退出redis

shutdown # 关闭redis

exit # 退出

4、redis的基本知识

redis 默认有16个数据库,redis.conf文件可以看到相关的配置,默认是第0个数据库

当然也可以使用select去切换数据库

image-20230802203232740

redis常见命令:

redis命令英文网命令 |雷迪斯 (redis.io)

redis命令中文网redis命令手册

select :数据库的切换

dbsize:当前数据库的大小

key *:查看所有的key

flushdb:是清空当前库

exists:判断key是否存在

move:移除key

expire key time:设置key的过期时间(单位是秒)

ttl:可以查看剩余时间

type key:查看当前key的类型

redis单线程为什么还是这么快

核心:redis是将所有的数据全部放在内存中的,多线程(CPU上下文切换:耗时的操作!!!),对于内存系统来说,如果没有上下文的切换效率就是最高的

redis从6.0开始已经开始支持多线程了,在redis.conf文件中io-threads配置可以开启

这回终于把Redis多线程讲清楚了! - 知乎 (zhihu.com)

(Another Redis Desktop Managerr)redis图形界面工具

5、Redis五种类型

1、String类型

127.0.0.1:6379> set name zl  #
OK
127.0.0.1:6379> get name
"zl"
127.0.0.1:6379> append name "hello" #在指定key的value上追加字符串,如果key不存在的话就等于 set的功能
(integer) 7
127.0.0.1:6379> get name
"zlhello"
127.0.0.1:6379> strlen name #获取字符串的长度
(integer) 7
127.0.0.1:6379>
127.0.0.1:6379>
##############################################################################################
设置浏览量views
127.0.0.1:6379> set views 0    
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views   #对浏览量进行+1
(integer) 1
127.0.0.1:6379>
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr viewa    
(integer) -1
127.0.0.1:6379> decr views   #对浏览量进行-1
(integer) 1
127.0.0.1:6379> key *
(error) ERR unknown command 'key'
127.0.0.1:6379> keys *
2) "viewa"
4) "views"
5) "name"
127.0.0.1:6379>
同时也可以设置步长 指定相应长度
##############################################################################################
127.0.0.1:6379> set name "zhou,hello"
OK
127.0.0.1:6379> get name
"zhou,hello"
127.0.0.1:6379> getrange name 1 3   #截取字符串 从下标0开始 相当于java subString()
"hou"
127.0.0.1:6379> getrange name 1 -1   #想到于get key
"hou,hello"
127.0.0.1:6379> setrange name 1 xx   #字符串替换 指定位置 和相应字符相当于java replace ()
(integer) 10
127.0.0.1:6379> get name
"zxxu,hello"
127.0.0.1:6379>
#####################################################################################
#setex (set with expire) 设置时间
#setnx (set if not exist) 不存在设置,存在就不会设置
setex key3 30 123   # 设置30秒后自动过期
setnx  mykey  hello  # key不存在创建,存在则创建失败
####################################################################################
批量设置
mset key1 v1 key2 v2 key3 v3  #批量创建key
mget key1 key2 key3
msetnx key1 v1 key4 v4   # msetnx 原子性操作,要么一起成功,要么一起失败
######################################################################################
①
set user:1 {name:dy,age:18}   # 创建一个对象
get user:1    # "{name:dy,age:3}"
② 巧妙的设计key值
mset user:{id}:{filed} value 
使用mget user:{id}:{filed} 
####################################################################################
getset #先获取值再设置值

String类似的使用场景:value除了是我们的字符串还可以是我们的数字

  • 计数器
  • 统计多个单位的数量
  • 粉丝数
  • 对象缓存存储

2、List类型

del names

lpush hy # 创建list添加数据

lpush hy1 hy2 hy3 hy3

lrange names 0 4 # 查看list列表names的0-4条记录
lpush list 1   #从左侧存值
lpush list 2
lpush list 3
rpush list 4   #从右侧存值

===============================
lrange list  0 2  #获取值 倒着取值
lrange list  0 -1 #取所有的值
lindex list 1     #从左边 下标取值  没有rindex
===============================
lpop list  #从左边移除值
rpop list  #从右边移除值

=====================
Llen   # 获取长度
lrem list 1 1  #移除指定的值  第一个参数是数量  第二个是移除的元素
=====================
ltirm key  start end #通过下标来截取key 
=====================
rpopLpush  key1 key2 #移除末尾的一个元素,将这个值又添加到开头 ,其中这两个key值可以相同
===========================
lset key index  value #lset可以通过指定key的指定下标位更新为value的值,当然他会先判断key是否存在
=========================================================
linsert key before|after  value1 value2#将value2的值插入到指定key中value1的前面或后面,当出现有重复的value值时,会插入到第一个value1的指定位置

小结

  • list实际上是一个链表,before Node after , left,right都可以插入值
  • 如果key不存在,创建新的链表
  • 如果key存在,新增内容
  • 如果移除了所有的值,空链表,页代表不存在!
  • 在两边插入或者改动值,效率最高!中间元素,相对来说效率会低一点~

消息排队!消息队列(Lpush Rpop),栈(Lpush Lpop)

3、set集合

Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。

Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。

集合中最大的成员数为 2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)。

sadd set "hello" #将set集合里面添加元素
smembers set #显示出set集合里面所有的元素
sismember set value #判断value里面的值是否存在set中
SREM key member1 [member2] 移除集合中一个或多个成员
SRANDMEMBER key [count] 返回集合中一个或多个随机数 随机抽选一个元素
SMOVE source destination member 将 member 元素从 source 集合移动到 destination 集合
===============================================================================
求共同关注
SDIFF key1 [key2] 返回给定所有集合的差集  key1-key2
SINTER key1 [key2] 返回给定所有集合的交集  key1交key2
SUNION key1 [key2] 返回所有给定集合的并集  key1并key2

4、Hash(哈希) Map集合

这个时候的value就是一个map集合了

HSET key field value 将哈希表 key 中的字段 field 的值设为 value 。

HMGET key field1 [field2] 获取所有给定字段的值

HMSET key field1 value1 [field2 value2 ] 同时将多个 field-value (域-值)对设置到哈希表 key 中。

hgetall key #获得所有的值 
获取格式
mapkey1 
mapvalue1
mapkey2
mapvalue2

hdel key filed删除指定的值 

HEXISTS key field 查看哈希表 key 中,指定的字段是否存在。

5、zset(有序列表)

Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

有序集合的成员是唯一的,但分数(score)却可以重复。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。

ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员,或者更新已存在成员的分数
ZRANGEBYSCORE key -inf +inf 通过分数返回有序集合指定区间内的成员 可以实现排序
ZRANGEBYSCORE key min max [WITHSCORES]  通过分数返回有序集合指定区间内的成员并显示scores值
ZCARD key 获取有序集合的成员数

6、三种特殊数据类型

1、gesopatial地理位置

geospatial操作

2、Hyperloglog基础

作用可以用来统计不重复的数(一个网站的访问量)

基数?

A={1,3,5,7,7,8 }

B={1,3,5,7,8}

基数:不重复的数,可以接受误差

适用场景:统计一个网站的访问量,一个ID多次访问只能算作是一次访问,当然可以使用set无序列表来统计,当统计的数量庞大时

使用set列表会非常的占用内存空间。

HyperLogLog 数据结构可用于仅使用少量恒定内存对集合中的唯一元素进行计数,特别是每个 HyperLogLog 的 12k 字节(加上键本身的几个字节)。观测集的返回基数不精确,但近似标准误差为 0.81%,

基本命令

=================================
pfadd key element... #添加一个或多个元素到hyperloglog集合中
pfcount key #查看key的元素个数
pfmerge destkey [sourcekey [sourcekey ...]] #将多个 HyperLogLog 值合并为一个近似的唯一值 源 HyperLogLog 的观察到的集合的并集的基数 结构。计算合并的 HyperLogLog 设置为目标变量,即 如果不存在,则创建(默认为空的HyperLogLog)。

3、BitMaps位图场景

位存储

位存储的应用场景:位示图来统计地址快的使用情况,可以表示用户在线与不在线,打卡

setbit sign 1 0 #表示第一天没有打卡
getbit sign 1  #获得第一天的打卡情况
bitcount sign  [start,end]#可以统计在某个范围内的sign情况

7、新增redis类型

1、stream流

Redis Stream | 菜鸟教程 (runoob.com)

stream流就是redis中的mp

官方介绍

Redis 流是一种数据结构,其作用类似于仅追加日志,但也实现了多个操作来克服典型仅追加日志的一些限制。其中包括O(1)时间的随机访问和复杂的消费策略,如消费者群体。 您可以使用流实时记录和同时联合事件。 Redis 流用例的示例包括:

  • 事件溯源(例如,跟踪用户操作、点击等)
  • 传感器监控(例如,现场设备的读数)
  • 通知(例如,将每个用户的通知记录存储在单独的流中)

Redis 为每个流条目生成一个唯一的 ID。 您可以在以后使用这些 ID 检索其关联的条目,或读取和处理流中的所有后续条目。

实训 模拟手机验证发送

实现功能:

  1. 输入手机号,点击发送后随机生成六位数字码,2分钟有效
  2. 输入验证码,点击验证,返回成功或者失败
  3. 每个手机号每天只能输入3次

实现步骤:

  1. 生成随机六位数字验证码
  2. 把验证码放在redis里面,设置过期时间120秒
  3. 判断验证码是否一致 只需要去除相应的key的value值与输入的值进行比较
  4. 使用incr每次发送加一
package com.zl.note.controller;

import com.zl.note.uitls.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Calendar;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.TimeUnit;


@RestController
public class sendPhoneMsg {
    @Autowired
    @Qualifier("redis")
    private RedisTemplate redisTemplate;
    @Autowired
    private RedisCache redisCache;
    @RequestMapping("/sendmsg")
    public void SendPhone(){
        String code = getCode();
        sendmsg("111",code);
    }
    //生成验证码
    public String getCode(){
        Random random = new Random();
        String code="";
        for (int i  = 0; i  < 6; i ++) {
             code+= random.nextInt(10);
        }
        return code;
    }
    //发送验证码
    public void sendmsg(String phone,String code){
        String count=phone+"count";
        String msg=phone+"msg";
        Integer o = (Integer) redisTemplate.opsForValue().get(count);
        //需要获取当前时间
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        long todayZero = calendar.getTimeInMillis();
        long currentTime = System.currentTimeMillis();
        long scondely=todayZero+86400000;
        long delay=(scondely-currentTime)/1000;
        if(Objects.isNull(o)){
            //表示用户还没有发送过验证码
            redisTemplate.opsForValue().setIfAbsent(count,0,delay, TimeUnit.SECONDS);
        }else if(o>2){
            System.out.println("你今天的次数已经用完");
            return ;
        }
        redisTemplate.opsForValue().set(msg,code);
        redisTemplate.opsForValue().increment(count);
        String o1 = (String) redisTemplate.opsForValue().get(msg);
        System.out.println("你发送的验证码为"+o1);
    }
}

8、redis事务操作

redis的本质:一组命令的集合

一个事务中的所有的命令都会被序列化,在事务执行过程中。会按照顺序执行!

一次性、顺序性,排他性!执行一系列的命令!

-----
命令1
命令2
命令3
-----

redis事务没有隔离级别的概念!就不会出现脏读

注意:redis单条命令式保护原子性的,但是redis事务不保证原子性!

redis的事务:

  • 开启事务(multi)
  • 命令入队(.......)
  • 执行事务(exec)

执行事务

127.0.0.1:6379> multi #开启事务
OK
--------------- #命令入队
127.0.0.1:6379(TX)> set k1 v1  
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get key2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
------------------- #执行事务
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) (nil)
4) OK
127.0.0.1:6379> 

取消事务

discard #取消事务

编译时异常(代码出现错误,队列中如果一个命令出现错误),执行事务时会显示错误其他没有出现错误的命令也会无法执行

127.0.0.1:6379(TX)> set k1 "hello"
QUEUED
127.0.0.1:6379(TX)> incr k1  #运行错误地方,字符串是不可以进行加1操作的
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> exec   #执行事务时,其他的命令还是正常的执行,没有出现错误
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
4) "v2"
127.0.0.1:6379> 

9、redis实现乐观锁

乐观锁:展现出很乐观,认为不会出现任何问题,在操作之前会检查元素是否发生改变

使用watch进行检测上锁
在watch上锁之后,在其他操作之前unwatch进行一个解锁操作

redis乐观锁在秒杀上有应用

10、通过Jedis操作Redis

什么是Jedis?

官方指定的java操作redis,

导入依赖

<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.3.0</version>
</dependency>

jedis里面的操作时使用jedis对象操作原生命令

11、SpringBoot-redis整合

导入依赖

<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

1、redis的底层实现

在SpringBoot2.X之后 使用的底层默认是lettuce,而不是jedis

lettuce和jedis之间的区别

jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用jedis pool连接池! BIO

lettce :采用netty,实列可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据,更像NIO模式

image-20230805095542378

2、SpringBoot整合redis源码解析

SpringBoot所有的配置类,都会有一个自动配置类XXXAutoConfiguration,自动配置类都会绑定一个properties配置文件XXXProperties,根据配置文件可以知道配置类有什么属性可以进行配置

在SpringBoot-configure的文件的/META-INF/spring.factories 里面搜索redis找到redis相关的依赖

image-20230805101225274

发现redis的配置类RedisAutoConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
    

   @Bean
   @ConditionalOnMissingBean(name = "redisTemplate")  //表示在SpringBoot里面没有redisTemplate改方法会生效,以为这我们可以自己重新redisTemplate方法代替代给方法
   @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
   public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
      RedisTemplate<Object, Object> template = new RedisTemplate<>();
      template.setConnectionFactory(redisConnectionFactory);
      return template;
   }

   @Bean
   @ConditionalOnMissingBean
   @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
   public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
      StringRedisTemplate template = new StringRedisTemplate();
      template.setConnectionFactory(redisConnectionFactory);
      return template;
   }
}

image-20230805103005738

引用RedisCemplate

@Autowirde
private RedisTemplate redisTemplate 

RedisTemplate里面的Filed
private final ValueOperations<K, V> valueOps = new DefaultValueOperations(this);  //相当于String类型,操作String类型
private final ListOperations<K, V> listOps = new DefaultListOperations(this);    //list类型
private final SetOperations<K, V> setOps = new DefaultSetOperations(this);         //set类型
private final StreamOperations<K, ?, ?> streamOps = new DefaultStreamOperations(this,  ObjectHashMapper.getSharedInstance()); //stream流类型
private final ZSetOperations<K, V> zSetOps = new DefaultZSetOperations(this); //zset类型
private final GeoOperations<K, V> geoOps = new DefaultGeoOperations(this);  //geospatial类型
private final HyperLogLogOperations<K, V> hllOps = new DefaultHyperLogLogOperations(this); //hyperloglog类型
private final ClusterOperations<K, V> clusterOps = new DefaultClusterOperations(this);

有相应的opsForXXX()去获得redis十大基本类型的实列对象

image-20230805104302326

这样使用原生的指令太繁琐了,建议是将redis的相关操作封装称为一个redisUitls工具类

解决中文乱码问题

默认的是jdk序列化

image-20230805140130580

解决方案:

①可以使得对象类去实现Serializable接口

②将RedisTemplate的默认序列化改成json序列化

package com.zl.note.configuration;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.context.annotation.Bean;
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;

public class RedisConfig {
    @Bean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        //Json序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om=new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        //String的序列化
        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;
    }
}

redis工具类

package com.zl.note.uitls;

import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;

@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisCache {
    @Resource
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key   缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key      缓存的键值
     * @param value    缓存的值
     * @param timeout  时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout) {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @param unit    时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit) {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key) {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key) {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection) {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key      缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList) {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key) {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key     缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext()) {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key) {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key   Redis键
     * @param hKey  Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey) {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 删除Hash中的数据
     *
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey) {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key   Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern) {
        return redisTemplate.keys(pattern);
    }
}

实列 秒杀案例

package com.zl.note.controller;

import com.zl.note.pojo.ResponseResult;
import com.zl.note.uitls.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Objects;
import java.util.UUID;


@RestController
public class sendPhoneMsg {
    @Autowired
    @Qualifier("redis")
    private RedisTemplate redisTemplate;
    @Autowired
    private RedisCache redisCache;
    @RequestMapping("/sendmsg")
    public ResponseResult SendPhone(){
     //生成用户ID和商品ID
        String userid= "38595296-27c2-4a43-bdb4-1dd294c514f1";
        String produceId=UUID.randomUUID().toString();
        if(miaosha(userid,produceId)){
            return new ResponseResult(200,"秒杀成功",null);
        }
        return new ResponseResult(500,"秒杀失败",null);
    }
     public boolean miaosha(String userid,String produceId){

        //判断用户ID和商品ID是否都存在
         if(Objects.isNull(userid)||Objects.isNull(produceId)){
             System.out.println("操作异常");
             return false;
         }
         redisTemplate.watch("kc");
         //获取商品库存量
         Integer kc= (Integer) redisTemplate.opsForValue().get("kc");
         if(Objects.isNull(kc)){
             System.out.println("活动没有开始");
             return false;
         }
         //判断用户ID是否已经抢到了
         if(redisTemplate.opsForHash().hasKey("success:userid",userid)){
             System.out.println("你已经成功抢到商品,不可以重复抢购");
             return false;
         }
         //库存小于1时,表示已经抢完
         if(kc<1){
             System.out.println("手慢了,没有抢到");
             return false;
         }
         //秒杀过程
         redisTemplate.opsForValue().decrement("kc");
         redisTemplate.opsForHash().put("success:userid",userid,produceId);
         return true;
     }
}

模拟秒杀并发测试

1、安装ab(apache bench)是apache下的一个工具,主要用于做web站点的压力测试

yum install httpd-tools            #Centos安装
sudo apt-get install apache2-utils   #ubuntu安装

参数详解ab工具使用详解_李硕硕的博客-CSDN博客

-n(常用) 发出x个请求
-c(常用) 并发一次发出的多个请求数,也就是模拟x个并发
-t(常用) 将花费在基准测试上的时间限制为最长秒,在x秒内发请求
-s(常用) 等待每个响应的最大超时秒数,默认30秒
-p 发送POST请求时需要上传的文件,此外还必须设置-T参数。
-u 发送PUT请求时需要上传的文件,此外还必须设置-T参数。
-T 内容类型用于POST/PUT数据的内容类型标题,默认值为text/plain。例如:application/x-www-form-urlencoded,

操作格式 : Usage: ab [options] [http[s]😕/]hostname[:port]/path

ab -n 1000[请求参数] -c 100[并发参数] -p 文件[提交参数]

ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.2.128:88/sendmsg

问题出现

1、连接超时问题(使用连接池可以解决) 超卖问题(使用乐观锁)

原因是乐观锁导致很多请求都失败了。先点的没秒到,后点的秒杀到了

每次连接redis服务带来的消耗,把连接好的实例反复利用

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();
                //这些相应的配置项也可以在application配置文件中配置
               poolConfig.setMaxTotal(200);
               poolConfig.setMaxIdle(32);
               poolConfig.setMaxWaitMillis(100*1000);
               poolConfig.setBlockWhenExhausted(true);
               poolConfig.setTestOnBorrow(true);  // ping  PONG
                //建立连接
               jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379, 60000 );
            }
         }
      }
      return jedisPool;
   }

   public static void release(JedisPool jedisPool, Jedis jedis) {
      if (null != jedis) {
         jedisPool.returnResource(jedis);
      }
   }

}

volatile关键字的定义:Java:java学习笔记之volatile关键字的简单理解和使用_JMW1407的博客-CSDN博客

由于我使用的是lettuce

package com.zhoulei.mybatis_plus01.controller;

import com.sun.corba.se.impl.orbutil.concurrent.Sync;
import com.zhoulei.mybatis_plus01.pojo.ResponseResult;
import lombok.Synchronized;
import org.apache.ibatis.transaction.Transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Objects;
import java.util.UUID;
@RestController
public class sendPhoneMsg {
    @Autowired
    @Qualifier("redis")
    private RedisTemplate redisTemplate;
    @RequestMapping("/sendmsg")
    public ResponseResult SendPhone(){
     //生成用户ID和商品ID
        String userid= UUID.randomUUID().toString();
        String produceId=UUID.randomUUID().toString();
        if(miaosha(userid,produceId)){
            System.out.println("操作成功");
        }
        return new ResponseResult(500,"秒杀失败",null);
    }
    @Synchronized   //加锁
     public  boolean miaosha(String userid,String produceId){
             //判断用户ID和商品ID是否都存在
             if (Objects.isNull(userid) || Objects.isNull(produceId)) {
                 System.out.println("操作异常");
                 return false;
             }
             redisTemplate.watch("kc");
             //获取商品库存量
             Integer kc = (Integer) redisTemplate.opsForValue().get("kc");
             if (Objects.isNull(kc)) {
                 System.out.println("活动没有开始");
                 return false;
             }
             //判断用户ID是否已经抢到了
             if (redisTemplate.opsForHash().hasKey("success:userid", userid)) {
                 System.out.println("你已经成功抢到商品,不可以重复抢购");
                 return false;
             }
             //库存小于1时,表示已经抢完
             if (kc < 1) {
                 System.out.println("手慢了,没有抢到");
                 return false;
             }
             //开启事务
             redisTemplate.setEnableTransactionSupport(true);
             RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
             connection.multi(); // 开启事务
             //秒杀过程
             redisTemplate.opsForValue().decrement("kc");
             redisTemplate.opsForHash().put("success:userid", userid, produceId);
             connection.exec();
             //断开连接,归还到连接池
            RedisConnectionUtils.releaseConnection(connection, redisTemplate.getConnectionFactory());
             return true;
     }
}

  #配置lettuce连接池
  redis:
    port: 6379
    lettuce:
      pool:
        max-idle: 32 #连接池中最小的空闲连接
        max-wait: 5000ms  #最大堵塞时间
        max-active: 200 #最大连接数
        time-between-eviction-runs: 1ms #eviction线程调用时间间隔
    connect-timeout: 2000ms #连接超时时间

小白踩坑:出现 ERR EXEC without MULTI 显示错误表示执行前没有开启事务。我当时使用redisTemplate.multi()开启事务,出现错误。

关于RedisTemplate的ERR EXEC without MULTI错误_0000Joker0000的博客-CSDN博客

因为RedisTemplate默认是不开启事务支持的,而且在执行exec方法时,会重新创建一个连接对象(或者从当前线程的ThreadLocal中拿到上一次绑定的连接)。所以,我们在不开启事务的情况下,自己在外面执行的multi方法时完全不会生效的(因为连接对象都换了)~

// 在事务中使用相同的Redis连接进行进一步的操作。线程绑定的对象将在事务完成时被同步删除。
RedisConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
    //建立一个新的连接对象
    holderToUse = new RedisConnectionHolder(connection);
} else {
    //中拿到上一次绑定的连接
    holderToUse.setConnection(connection);
}holderToUse = new RedisConnectionHolder(connection);

第二种方法有用对于redis7来说,使用setEnableTransactionSupport(true)没有作用了,源码已经做了修改

同时也可以使用

RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
connection.multi(); // 开启事务

2、库存遗留问题(使用lua脚本解决)

导致原因:乐观锁导致,当有许多的用户抢到同一个商品,当一个用户抢购成功,之后就会修改版本号,导致其他的用户无法购买

3.库存遗留问题

当我们固定请求量的时候,因为我们的乐观锁机制,会导致很多请求都被拒绝了。我们在模拟高并发的时候,会设定一个请求量。假设我库存为10,设置100个请求,乐观锁机制导致93个请求失效了,那么我就遗留下来3个库存,这就是库存遗留问题。库存遗留问题同样也是多请求失效问题,有效请求太少了,用户体验不好

解决办法是悲观锁,将所有都加锁,但是因为redis不能实现悲观锁,所以我们使用lua来解决问题,lua脚本的本质是使用单线程队列,也能够实现悲观锁的效果

解决办法就是使用lua脚本

local userid=KEYS[1]; 
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local usersKey="sk:"..prodid.":usr'; 
local userExists=redis.call("sismember",usersKey,userid);
if tonumber(userExists)==1 then 
  return 2;
end
local num= redis.call("get" ,qtkey);
if tonumber(num)<=0 then 
  return 0; 
else 
  redis.call("decr",qtkey);
  redis.call("sadd",usersKey,userid);
end
return 1;
/**
 * 使用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;
   }
}

原理

将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。

但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。

利用lua脚本淘汰用户,解决超卖问题。

redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题

12、redis.conf配置

Redis:redis.conf配置文件 - 及配置详解 - 怒吼的萝卜 - 博客园 (cnblogs.com)

1、units单位

  1. 配置大小单位,开头配置了一些基本的度量单位,支支持bytes,不支持bit
  2. 他对大小写不敏感

image-20230805162555127

2、include包含

image-20230805162915040

好比improt可以导入其他的配置文件

3、network网络

port 6379  #配置redis服务器的端口号
bind=127.0.0.1 #绑定IP地址,默认是绑定本机的IP地址,表示只有本机可以访问,如果是远程的话,我们可以将其注释掉,可以接受所有的IP地址,也可以使用通配符 *来表示所有的IP地址,
protected-mode yes #保护模式  

#此参数确定了TCP连接中已完成队列(完成三次握手之后)的长度, 当然此值必须不大于Linux系统定义的/proc/sys/net/core/somaxconn值,默认是511,而Linux的默认参数值是128。
#当系统并发量大并且客户端速度缓慢的时候,可以将这二个参数一起参考设定。该内核参数默认值一般是128,对于负载很大的服务程序来说大大的不够。一般会将它修改为2048或者更大。
#在/etc/sysctl.conf中添加:net.core.somaxconn = 2048,然后在终端中执行sysctl -p
#在高并发环境下你需要一个高backlog值来避免慢客户端连接问题
tcp-backlog 511

#此参数为设置客户端空闲超过timeout,服务端会断开连接,为0则服务端不会主动断开连接,不能小于0
timeout 0

4、general通用

daemonize yes#守护进程运行会有后台形式进行
pidfile /var/run/redis_6379.pid   #如果我们以后台的方式运行的话,我们需要指定一个pid文件。

# debug (a lot of information, useful for development/testing)   在开发或测试环境
# verbose (many rarely useful info, but not a mess like the debug level)   
# notice (moderately verbose, what you want in production probably)   在生产环境
# warning (only very important / critical messages are logged)
loglevel notice   日志

logfile "" #配置日志文件。空字符串的话,日志会打印到标准输出设备。后台运行的redis标准输出是/dev/null

5、快照

认识一下快照,数据存储的某一时刻的状态记录,换句话来说就是将我们的数据进行一个监听,到了规定的时间点后就会将数据保存下来,使其完成持久化。

redis中的快照,在规定时间内,执行了多少次操作,则会持久化到文件(.rdb .aof)

redis是内存数据库,如果没有持久化操作的话,就会断电即失的

save 3600 1  #在3600S内至少有一个key进行操作就会进行保存
save 300 100  #在300S内至少有100个key进行操作就会进行操作
save 60 10000  #如果在60S内有10000个key进行操作就会进行保存
    
#当RDB持久化出现错误后,是否依然进行继续进行工作,yes:不能进行工作,no:可以继续进行工作,可以通过info中的rdb_last_bgsave_status了解RDB持久化是否有错误
stop-writes-on-bgsave-error yes


#配置存储至本地数据库时是否压缩数据,默认为yes。Redis采用LZF压缩方式,但占用了一点CPU的时间。
#若关闭该选项,但会导致数据库文件变的巨大。建议开启。
rdbcompression yes

#是否校验rdb文件;从rdb格式的第五个版本开始,在rdb文件的末尾会带上CRC64的校验和。
#这跟有利于文件的容错性,但是在保存rdb文件的时候,会有大概10%的性能损耗,所以如果你追求高性能,可以关闭该配置
rdbchecksum yes

dir ./#rdb文件保存地址,默认是在本文件保存

6、scurity安全

# requirepass foobared  #设置密码,redis默认是没有密码,可以在配置文件中设置密码,也可以使用命名去设置密码。

7、限制client

# maxclients 10000  #最大客户端的数量
# maxmemory <bytes> #最大的内存
# maxmemory-policy noeviction  #当内存满时的策略
volatile-lru:只对设置了过期时间的key进行LRU(默认值)
allkeys-lru : 是从所有key里 删除 不经常使用的key
volatile-random:随机删除即将过期key
allkeys-random:随机删除
volatile-ttl : 删除即将过期的
noeviction : 永不过期,返回错误

8、APPEND ONLY MODE aof模式

appendonly no#默认是不开启aof模式,默认是使用rdb方式持久化,大部分所有的情况下,rdb完全够用!
appendfilename  "appendonly.aof" #持久化的文件名字
#appendfsync always #每次修改都会sync。消耗性能
appendfsync everysec #每秒执行一次sync 可能会丢失1S的数据!
#appendsync no   #不执行sync,这个时候操作系统自己同步数据,速度最快!

13、持久化

Redis是内存数据库,如果不将内存中的数据库状态保存在磁盘上,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以Redis提供了持久化功能!主要分为两类①rdb ②aof

1、RDB

img

RDB是在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读入内存当中的

备份是如何执行的
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程结束,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何io操作的,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据恢复完整性不那么敏感,那RDB方式要比AOF方式更加的高效RDB的缺点是最后一次持久化后的数据可能丢失

注意:系统一般默认是rdb保存

有时候在生产环境我们会将这个文件进行备份!

rdb默认文件时dump.rdb

Fork

  • Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
  • 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术
  • 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程

触发机制

1、save的规则满足的情况下,会自动的触发rdb规则

save特点:save时只管保存,其它不管,全部阻塞。手动保存。不建议使用。一般我们不设置save指令或者给他传空字符串 save "",不使用它。

推荐使用bgsave:Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。注意我们的bgsave是手动持久化的命令,这里的save是自动持久化

2、执行flushall命令,也会触发我们的rdb规则

3、退出redis,也会产生rdb文件!

当触发rdb机制之后就会自动的生成一个dump.rdb文件

如何使用rdb文件恢复数据

1、只需要将rdb文件放在我们的redis启动目录就可以,redis启动的时候会自动的检查dump.rdb恢复其中的数据!

2、查看需要存在的位置

image-20230807101649719

优点:

1、适合大规模的数据恢复!

2、对数据的完整性要求不高!

缺点:

1、需要一定的时间间隔进程操作!如果redis意外宕机了,这个最后一次修改数据就没有了!

2、fork进程的时候,会占用一定的内存空间!!

2、AOF

将我们所有的命令都记录下来,history,恢复的时候就打这个文件全部执行一遍!

aop(append only file)他是以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

1.AOF配置

aof默认是不开启的,可以再开redis.conf中配置文件名称,默认为不开启

appendonly no # 是否开启aof,默认不开启aop

appendfilename "appendonly.aof" # aof生成文件名,默认持久化文件名

aof文件的保存路径,同rdb路径一致

aof与rdb同时开启,系统默认读取aof

2.AOF启动、修复、恢复

AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载。

正常恢复

  1. 修改默认的appendonly no,改为yes
  2. 将有数据的aof文件复制一份保存到对应目录(查看目录:config get dir)
  3. 恢复:重启redis然后重新加载

异常恢复

当aof文件损坏时,我们可以修复损坏的aof文件

  1. 修改默认的appendonly no,改为yes
  2. 备份被写坏的aof文件
  3. 如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof --fix appendonly.aof进行恢复
  4. 重启redis,重新加载

3.AOF同步频率

在配置文件中的 appendsync有三个模式

  • always,始终同步,每次Redis的写入都会立刻记录日志,性能差但完整性好
  • everysec,每秒进行一次同步,如果宕机,本秒数据可能丢失
  • no,不主动进行同步,把同步时机交给操作系统

4、AOF的重写机制

1、rewirte机制

为什么AOF需要有重写机制?

  1. AOF可以无限的追加命令,这会使得我们AOF文件越来越大,并却在这些命令中有许多的冗余命令,

    ①比如我们执行incr key 一直执行100下,其实就可以使用步长(incrby key 100)或者是直接赋值大key中(set key 100)

    ②多条写命令可以合并为一个,如lpush list a、lpush list b、 lpush list c 可以转化为:lpush list a b c。

    ③一些过期的数据

AOF的重写原理

AOF的工作原理是将写操作追加到文件中,文件的冗余内容会越来越多。所以聪明的 Redis 新增了重写机制。当AOF文件的大小超过所设定的阈值时,Redis就会对AOF文件的内容压缩。
Redis 会fork出一条新进程,读取内存中的数据,并重新写到一个临时文件中。并没有读取旧文件。最后替换旧的aof文件。


为了合并重写AOF的持久化文件,Redis提供了bgrewriteaof命令。收到此命令后,Redis将使用与快照类似的方式将内存中的数据以命令的方式保存到临时文件中(并不是只是简单的去读取,而是会执行,将其结果保存),最后替换原来的文件,以此来实现控制AOF文件的合并重写(会将重写过程中接收的的新的指令和生成新的重写后AOF文件中的指令进行合并)。

2.核心配置no-appendfsync-on-rewrite

这个配置,表示是否将重写写入aof文件

- 如果`no-appendfsync-on-rewrite yes` ,#不写入aof文件,只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
- 如果`no-appendfsync-on-rewrite no`,# 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)
5.写操作配置

当我们达到写操作阈值的时候,我们会将缓冲区内的数据持久化

auto-aof-rewrite-percentage:100 # 设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
auto-aof-rewrite-min-size # 设置重写的基准值,最小文件64MB。达到这个值开始重写

例:

文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB

系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,

如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。

6、混合持久化

配置

aof-use-rdb-preamble yes#开启混合持久化配置

aof 按照文本的方式写入文件,但是文本写入成本是比较高的,redis 就引入了 “混合持久化” 的方式,结合了 rdb 和 aof 的特点~在开启混合持久化的情况下, aof 重写时会把 redis 的持久化数据,以 RDB 的格式写入到新的 AOF 文件的开头,之后的数据再以 AOF 的格式化追加的文件的末尾,这样做,既可以避免因 aof 文件较大影响 redis 启动速度,又能防止 rdb 导致的一段时间内的数据丢失.

14、主从复制

什么是主从复制?

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);

数据的复制是单向的,只能由主节点到从节点。

默认情况下,每台Redis服务器都是主节点,且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

目前很多中小企业都没有使用到 Redis 的集群,但是至少都做了主从。有了主从,当 master 挂掉的时候,运维让从库过来接管,服务就可以继续,否则 master 需要经过数据恢复和重启的过程,这就可能会拖很长的时间,影响线上业务的持续服务。

主从复制的作用主要包括:

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  • 高可用基石:主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

搭建redis集群

  1. 创建/myredis文件夹
  2. 复制redis.conf配置文件到文件夹中
  3. 配置一主两从,创建三个配置文件
    1. redis_6379.conf
    2. redis_6380.conf
    3. redis_6381.conf

实现是我们将redis.conf作为公共配置文件,在其他redis中include(引入)公共配置文件,在做自己单独的配置

公共配置文件

daemonize yes # 开启守护进程
appendonly no # 关闭aof

redis_6379.conf

include /myredis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb

redis_6380.conf

include /myredis/redis.conf
pidfile /var/run/redis_6380.pid
port 6380
dbfilename dump6380.rdb

redis_6380.conf

include /myredis/redis.conf
pidfile /var/run/redis_6381.pid
port 6381
dbfilename dump6381.rdb
  1. 启动redis

    1. redis-serve redis6379.conf
    2. redis-serve redis6380.conf
    3. redis-serve redis6381.conf
    
  2. 查看redis情况

    ps -ef|grep redis #查看进程
    info replication #redis客户端中输入命令,查看本机信息(其中包括主机或者是从机的相关信息)
    
  3. 从机加入主机

    1. slaveof 主机ip 端口号 #在从机执行这个命令可以将自己作为从机加入主机
    2. 在6380与6381上面执行`slaveof 127.0.0.1 6379`
    
  4. 测试主写从读

  5. 配置文件 salveof 主机ip 端口号可持久化配置

资料来源(王富贵 (lmlx66.top)

注意:我们刚刚所使用的是命令行使用命令来配置主从,这种方式不具有持久性(在从机shutdown之后重启,需要再次任大哥(主机),)想要从机重新启动之后不需要配置主机就要在相应的从机配置文件中配置主机的IP和端口

# replicaof <masterip> <masterport>

我们测试几个问题

1.当我们从机挂掉之后,重启还能加载之前的数据么?

可以

2.从机是否可以写操作?

不可以

3.主机挂掉了,从机是上位还是原地待命?

在默认模式下,从机是原地待命的

4.主机挂掉又回来了,从机是否还能顺利复制?

只要主机的数据还在,从机就可以进行之后的操作

5.其中一台从机挂掉之后,依照原有它能跟上大部队么?

可以,从机加入之后,会将主机所有数据加载进来

主从复制原理

  1. 从服务器(slave)启动成功连接到主服务器(master)后,从服务器(slave)会发送一个sync命令给主服务器(master)master接收到命令,启动后台的存盘进程,用时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送的整个文件到slave,并完成一次完全同步
  2. 主服务器接到从服务器发送过来同步消息,把主服务器数据进行持久化,rdb文件,把rdb文件发送到从服务器,从服务器拿到rbd文件之后进行读取(这个过程我们叫全量复制
  3. 主服务器进行写操作之后,他会将所有收集到的命令传给从服务器,完成同步(这个过程我们叫增量复制
  4. 注意只要重新连接主服务器,我们就会进行一次全量复制

第二个模型

image-20230807162700359

将79号作为主机,80作为79的从机,81又作为80的从机

注意的:这个80这个中间的还是只能充当一个从机的身份,不可以有写的操作,(奴隶的奴隶还是我的奴隶)

上一个从服务器(slave)可以是下一个从服务器(slave)的主服务器(master),从服务器(slave)同样可以接收其他从服务器(slave)的连接和同步请求,那么该从服务器(slave)作为了链条中下一个的主服务器(master), 可以有效减轻主服务器(master)的写压力,去中心化降低风险

谋朝篡位

当79主机发生了宕机下线之后,就会出现一个群龙为首的场面,没有办法写入数据,这是就要在从机当中选出一个从机变成主机,

从机变成主机需要执行 slaveof no one指令将该从机变成主机。这是需要手动去需要配置

思考:

  1. 当原来的主机(leader)恢复之后,会不会回到原来的模型?

    不会回到原来的模型,80任然是主机,要想恢复之前的模型,需要重新手动配置,将80机变成79的从机

15、哨兵模式(重点)

哨兵模式为了解决上面出现主机宕机之后需要手动配置主机的情况。哨兵模式将帮助我们自动的配置主机。

哨兵也是一个单独的进程,用于检测master主机是否发生了宕机,在工作中一般使用哨兵集群,因为只使用一个哨兵的话,这个哨兵可能也会发生意外出现宕机,导致无法配置出新的主机。

img

哨兵是如何进行监控和自动选择下一代主机的呢?

哨兵集群认为主节点宕机的依据:
根据quorum的值决定,比如默认是2,这意味着如果2个哨兵认为该主节点宕机了,就会选举从节点作为主节点(故障转移),quorum的值是 {哨兵集群的数量}/2 + 1,即超过集群数量半数,比如现在哨兵集群有5台节点,这quorum的值应该为3,刚好超过半数,防止脑裂

如果master服务器发生了宕机,哨兵会通过给master发送消息如果mater没有做出回应就哨兵就会主观的认为master宕机了称为主观下线这时候系统不会马上进行failover过程。当后面哨兵也检测到master宕机,并且达到一定的数量之后,哨兵就会进行一次投票(使用投票算法),进行failover【故障转移】操作,切换成功后,就会发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,称为客观下线

1.如何使用

  1. 先调整成一主(6379)二从(6380、6381)模式
  2. 在自定义的/myredis目录下新建sentinel.conf文件,名字不能错

sentinel.conf

# sentinel monitor 起名 被监视主机ip number(数字表示有几个哨兵同意才切换)
# 样例
sentinel monitor mymaster 127.0.0.1  1
  1. 同时,我们的哨兵是一个服务,单独启动他
# 启动哨兵服务,通过后面这个配置文件
redis-sentinel  /myredis/sentinel.conf 

当我们的主机(6379)下线,哨兵会从从机中选取一个从机(6380)当主机

同时原主机(6379)会保留信息,当他再次上线,就只能当新主机(6380)的从机

2、从机选取主机规则

从节点集群中选择slave-priority配置最小的值那台从节点作为主节点,如果不存在slave-priority配置,这选择从节点集群中同步数据最多,偏移量最大的从节点作为主节点,如果还不存在,这选择runid最小的值作为主节点(即最新启动的从节点)

注意:在redis6中,slave-priority的名字叫 replica-priority 100 这个值为 0 表示 replica 节点永远不能被提升为 master 节

偏移量是指获得原主机数据最全的

每个redis实例启动后都会随机生成一个40位的runid

16、Redis应用问题解决

1、缓存穿透

访问不存在的某一个值,redis以为没有缓存,给到数据库去查,大量访问造成数据库崩溃。

key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库

image-20230809224216337

解决办法

  1. 对空值缓存: 如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟
    • 缺点是只能作为临时解决穿透的方案
  2. 设置可访问的名单(白名单): 使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问
    • 效率不高,占用也比较大
  3. 采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中
    • 它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难
  4. 进行实时监控: 当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务(一般都是被黑才会穿透)

2、缓存击穿

访问存在的某一个值,访问量大,缓存过期的瞬间,会去数据库访问,数据库崩溃。一般是热点数据key过期

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮

  1. 数据库访问压力瞬时增大
  2. redis里面没有出现大量的key过期
  3. redis正常运行

例:redis某个key存在但过期了,大量访问这个key,因为key过期了,于是转向查数据库,照成数据库崩溃

解决办法

key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题

  1. 预先设置热门数据: 在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长

  2. 实时调整: 现场监控哪些数据热门,实时调整key的过期时长

  3. 使用锁:

    (能解决,但是效率低)

    1. 就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db
    2. 先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key
    3. 当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key
    4. 当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法

​ 4.加互斥锁

分布式锁:使用分布式锁,保证对于每一个key,同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方法将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。

3、缓存雪崩

在特定时间内,大量的key过期,虽然每一个访问量不多,但是加起来去访问数据库,照成数据库崩溃

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮

解决办法

  1. 构建多级缓存架构: nginx缓存 + redis缓存 +其他缓存(ehcache等)
  2. 使用锁或队列: 用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
  3. 设置过期标志更新缓存: 记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存
  4. 将缓存失效时间分散开: 比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件

17、分布式锁

1、什么是分布式锁

在传统单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。

但是在分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机并发控制锁策略失效,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁的由来。

当多个进程不在同一个系统中,就需要用分布式锁控制多个进程对资源的访问。

实现分布式锁的常见方法

  1. 数据库乐观锁;

  2. 基于ZooKeeper的分布式锁;

  3. 基于Redis的分布式锁:

2、基于redis实现分布式锁

基于Redis命令:SET key value NX EX max-lock-time

指令演变

setnx key value用锁的方式设置,如果没有释放,是不能操作他的

del key删除这个key,就可以解锁了

set key value ex second用锁的方式设置,ex代表设置过期时间,单位秒(防止死锁)

set key px millisecond用锁的方式设置,px代表设置过期时间,单位毫秒(防止死锁)

setnx key value如果不存在才设置,效果等同于 set key value nx相当于再次加锁

setxx key value如果不存在才设置,效果等同于 set key value xx相当于反向加锁

实现既上锁有设置过期时间的原子性操作

SET key value NX EX max-lock-time 设置nx锁,同时实现了设置过期时间。

setex key Thread1 expireTime 设置锁和过期时间

3、lettuce操作分布式锁

@GetMapping("testLock")
public void testLock(){
    //1.获取锁,或者叫设置锁,相当于set ne
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",3,TimeUnit.SECONDS);
    //2.获取锁成功、查询num的值
    if(lock){
        Object value = redisTemplate.opsForValue().get("num");
        //2.1判断num为空return
        if(StringUtils.isEmpty(value)){
            return;
        }
        //2.2有值就转成int
        int num = Integer.parseInt(value+"");
        //2.3把redis的num加1
        redisTemplate.opsForValue().set("num", ++num);
        //2.4释放锁,del
        redisTemplate.delete("lock");

    }else{
        //3获取锁失败、每隔0.1秒再获取
        try {
            Thread.sleep(100);
            testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

解决误删操作

误删产生的原因

当线程A首先抢到了锁并且上了锁设置了过期时间,当A线程在执行具体操作的时候,出现了服务器卡顿现象,导致A线程的锁过期时间已经到了,自动的删除了A线程的锁,这时B线程就会抢到这把锁,重新进行上锁操作,在B线程锁没有过期之前,A线程执行完毕需要手动的删除锁,由于A自己的锁已经自动的删除了,所以A线程就会手动的把B线程上的锁删除掉。

image-20230810121609142

解决方案

使用UUID来作为value充当唯一标识,在手动删除所之前,通过判断value值是否是本线程的UUID值,如果是就会删除锁,不是就不会删除锁。

@GetMapping("testLock")
public void testLock(){
    String uuid = UUID.randomUUID().toString();
    //1.获取锁,或者叫设置锁,相当于set ne
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3,TimeUnit.SECONDS);
    //2.获取锁成功、查询num的值
    if(lock){
        Object value = redisTemplate.opsForValue().get("num");
        //2.1判断num为空return
        if(StringUtils.isEmpty(value)){
            return;
        }
        //2.2有值就转成int
        int num = Integer.parseInt(value+"");
        //2.3把redis的num加1
        redisTemplate.opsForValue().set("num", ++num);
        //2.4释放锁,del
        if(uuid.equals((String)redisTemplate.opsForValue().get("lock"))){
            redisTemplate.delete("lock");
        }

    }else{
        //3获取锁失败、每隔0.1秒再获取
        try {
            Thread.sleep(100);
            testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

18、Redisson

基于setnx实现的分布式锁存在一些问题:

1、不可重入:同一个线程无法多次的获取同一把锁

​ 可能产生问题:当方法A去调用方法B但是方法B也要上锁,并且这是B和A的锁key值相同,即b也要获得这把锁,这时由于方法A无法执行下面的程序,导致死锁现象的产生。

2、不可重试:获取锁只尝试一次就返回false,没有重试机制。

3、超时释放:锁超时释放虽然可以避免死锁,但如果是业务时间比较长,也会导致锁释放,存在安全隐患。

posted @ 2023-08-13 23:00  zL66  阅读(20)  评论(0)    收藏  举报