Redis入门自学笔记springbootDataRedis类

Redis

1.开启redis

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mSivxxCB-1664353764895)(C:\Users\冯瑞涛\AppData\Roaming\Typora\typora-user-images\1662952321123.png)]

可以点击redis-server启动也可以在cmd控制面板中输入redis-server.exe ./redis.windows.conf来启动

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X9j4XuOs-1664353764896)(C:\Users\冯瑞涛\AppData\Roaming\Typora\typora-user-images\1662952490590.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xCuy5LVB-1664353764897)(C:\Users\冯瑞涛\AppData\Roaming\Typora\typora-user-images\1662952540521.png)]

启动成功之后的页面(使用cmd和配置文件的方法开启redis服务时,可以同时开启多个端口号不同的redis服务)

2.使用Spring Data Redis进行操作

1.定义

springdataredis是springboot提供的专门访问Redis的一套完整的操作方案,通常用于springboot构建的项目他将jedis的一些操作进行再封装,能更好的适配springboot框架。针对jedis的操作封装为了operation接口:

ValueOperations:简单K-V操作

SetOperations:set类型数据操作

ZSetOperations:zset类型数据操作

HashOperations:针对map类型的数据操作

ListOperations:针对list类型的数据操作

2.环境配置

2.1依赖导入

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

2.2yml文件配置

#Redis相关配置
redis:
  host: localhost
  port: 6379
 #password: 1020 #密码配置
  database: 0 #操作的是0好数据库,redis为我们提供了16个数据库
  jedis:
    #Redis连接池配置
    pool:
      max-active: 8 #最大连接数
      max-wait: 1ms #连接池最大紫色等待时间
      max-idle: 4 #连接池中最大空闲连接
      min-idle: 0 #连接池中最小空闲连接

2.3配置类

@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object,Object> redisTemplate=new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        return  redisTemplate;
    }
}

jar包自带的RedisTemplate对于接收的数据是默认jdkSerializationRedisSerializer()序列化方法的,若不进行配置存入的数据则会出现乱码,需要将序列化方法配置为StringRedisSerializer。

2.4字符串类型操作

/**
 *rids字符串类型操作
 */
@Test
public void testString() {
    //声明字符串操作类型ValueOperations
    ValueOperations valueOperations = redisTemplate.opsForValue();
    //添加属性(键值对)
    valueOperations.set("name","丫丫");
    //添加属性(key,value,time,单位)
    valueOperations.set("age","20",20L, TimeUnit.SECONDS);
    //存值并返回是否成功
    Boolean aBoolean = valueOperations.setIfAbsent("sex", "女");
    //用key取值
    String name = (String) valueOperations.get("name");
}

2.5列表类型操作

/**
 * redis列表操作
 */
public void testList(){
    //声明列表操作类
    ListOperations listOperations = redisTemplate.opsForList();
    //从左侧添加单个元素
    listOperations.leftPush("mylist","山月");
    //从右侧添加单个元素
    listOperations.rightPush("mylist","怅惘");
    //从左侧添加多个元素
    listOperations.leftPushAll("mylist","山海","湖泊","万物");
    //从右侧添加多个元素
    listOperations.rightPushAll("mylist","弓","箭");
    //从左侧弹出一个元素
    listOperations.leftPop("mylist");
    //从右侧弹出一个元素
    listOperations.rightPop("mylist");
    //取出mylist列表的所以元素
    List mylist = listOperations.range("mylist", 0, -1);
    //截取列表中的指定范围的元素
    listOperations.trim("mylist",2,4);
    //仅当列表存在时给列表添加值
    listOperations.leftPushIfPresent("mylist","量");
    //根据索引获取该位置的值
    listOperations.index("mylist",2);
}

2.6set类型操作

/**
 * 操作set类型
 */
public void testSet(){
    //声明set类型操作
    SetOperations setOperations = redisTemplate.opsForSet();
    //添加元素
    setOperations.add("myset","a","b","c","a");
    //取出所有元素
    setOperations.members("myset");
    //删除指定值
    setOperations.remove("myset","a","c");
}

2.7Zset类型操作

/**
 * 操作Zset类型
 */
public void testZset(){
    //声明Zset类型操作
    ZSetOperations zSetOperations = redisTemplate.opsForZSet();
    //添加属性(键,值,分数)按分数排名
    zSetOperations.add("myZset","a",1);
    zSetOperations.add("myZset","b",5);
    zSetOperations.add("myZset","c",3);
    zSetOperations.add("myZset","d",2.9);
    //取出指定键值对应的所有值
    Set myZset = zSetOperations.range("myZset", 0, -1);
    //为指定键值的指定值添加分数
    zSetOperations.incrementScore("myZset","d",2);
    //删除指定键值的指定一个或多个值
    zSetOperations.remove("myZset","a","c","d");
}

2.8通用操作

/**
 * 公共操作
 */
private void testCommon(){
    //获取所有key值
    Set keys = redisTemplate.keys("*");
    //检查key值是否存在
    Boolean myZset = redisTemplate.hasKey("myZset");
    //删除指定key
    redisTemplate.delete("myZset");
    //获得指定key值value的数据类型
    DataType myZsetType = redisTemplate.type("myZset");
}
Zset","a","c","d");
}

2.8通用操作

/**
 * 公共操作
 */
private void testCommon(){
    //获取所有key值
    Set keys = redisTemplate.keys("*");
    //检查key值是否存在
    Boolean myZset = redisTemplate.hasKey("myZset");
    //删除指定key
    redisTemplate.delete("myZset");
    //获得指定key值value的数据类型
    DataType myZsetType = redisTemplate.type("myZset");
}

3.redis事务和锁机制

3.1redis事务

redis事务是一个单独序列化,然后按一定序列化时的顺序逐条执行,其他命令无法对该事务造成

通过三个指令操作:multi(开始组队),exec(按顺序完成),discard(取消组队)

multi创建一个队伍:在取消或执行前的所有操作均属于该队伍管理,类似于开启事务

exec按顺序执行:按进入队伍的顺序逐条执行,类似于提交事务

discard取消组队:将组队取消,类似事务回滚

报错处理:

在multi阶段(组队阶段)报错,在进行exec时,所有操作都会失败(命令组队失败)

在multi阶段未报错,在exec阶段(执行阶段)报错则只有报错的那一条指令会失败,其他指令则会成功

由上得出:

redis没有隔离级别这一说法,所有事务均单独隔离

redis的事务不保证原子性

3.2reids锁机制

redis的锁机制乐观锁模式,也就是在多个线程同时获取信息时均能获取当时的信息,但只要有一个线程对信息的修改成功,那么信息的版本号就会发生改变,其他线程获取的信息就会过期,对信息的后续操作就会失败。

redis的锁机制由watch命名开启的,也就是在事务开启之前在不同的线程中对一个或多个key同时进行监听。

4.redis持久化

redis持久化:在固定时间周期性将redis快照入dump.rdb文件进行持久化。

4.1Rdb模式

rdb模式会在需要进行持久化时创建一个子线程fork线程进行持久化,先将数据存入一个临时存储文件,当固定时间周期到再一起将数据持久化到dump.rdb或覆盖掉前一次产生的dump.rdb文件,持久化成功后,子线程通知主线程,在整个持久化过程中主线程几乎不会进行任何的io操作,所以效率高。

rdb模式适合大量数据的恢复使用

不能保证恢复数据的一致性和完整性

节省磁盘空间,恢复速度快。

rdb模式最后一次持久化的数据容易丢失(在最后一次持久化时间周期未到前服务器宕机,则最后一次持久化不会成功)

4.2AOF模式

AOF模式并不是直接存储数据,而是存储关于写操作的指令,在redis重启时会去读append.aof文件,将文件中所有操作执行一遍来恢复数据。

客户端的写操作会被append追加到append.aof的尾部

AOF缓冲区根据配置文件中的持久化策略来进行持久化(always|erverysec|no)将操作sync同步到磁盘的AOF文件中

AOF文件大小超出范围会通过rewrite来进行重写压缩AOF文件

开启AOF:**appendonly **默认未no 将其改为 yes

持久化策略:appendfsync指令

always:每一次写操作都进行持久化

erverysec:每过一秒进行持久化

no:不自动进行持久化,将持久化时间交给操作系统

处理aof文件异常:通过**/usr/local/bin/redis-check-aof --fix**命令来进行修复

Redis主从复制

主从复制:一台主数据库多台从数据库,主从数据库数据实时同步,在从redis数据库中通过slaveof指令来实现指向主服务器格式为(slaveof 192.168.123.3 6379)此处主服务器的ip为192.168.123.3 redis的端口号为6379,使用slaveof no one来取消从属关系,并重新将该redis装维主数据库。

过程:

在从服务器连接上主服务时:

(1)从服务器会向主服务器发送数据同步请求(从服务器主动行为)

(2)主服务器会将自己的数据进行序列化进rdb文件中,再将rdb文件发送到从服务器,从服务器redis会读取这个文件,并完成数据同步。

(3)连接后每次主服务进行写操作之后,都会对从服务器进行数据同步(主服务器主动行为)

全量复制:主服务器的所有数据都复制进从服务器

增量复制:只对发生变化的数据进行复制

Reids集群

​ 我们用reids主从复制读写分离来减轻了服务器redis数据库读的压力 ,但大量的写操作对主服务器redis数据库造成的压力也很大,在大量写操作情况下不可避免地会是性能低下。在redis3.0之前,工程师们通过代理主机模式解决这个问题,redis3.0之后redis给出了一台成熟的解决方案,redis无中心集群。

主机代理模式:

​ 通过搭建一台代理主机来进行任务分配,服务器将请求发送到代理服务器,由代理服务器判断师针对哪一个模块发出的命令,而后又将请求发送给相应模块处理。

​ 代理模式需要的服务器量大,成本高,且不易维护。

redis无中心集群:

​ 同时针对不同模块创建针对模块的多个服务器,每一个模块的主服务器都能接收请求,然后对请求做出判断,在发送给相应模块,这一套方案既解决了相应的服务器写操作压力问题,也比代理主机策略的成本低,容易维护。

配置:(一个redis集群至少有三台主服务器)

​ 将redis启动的配置文件内容添加上:

​ cluster-enable yes

​ cluster-config-file nodes-6379.conf

​ cluster-node-timeout 15000

​ 集群中所有主服务器都得添加。

将以上配置的redis服务器群结合为一个集群:

​ 在整合集群前先进入redis安装根目录检查是否存在 redis-trib.rb文件,若存在则执行下一步,若不存 在则需要先安装ruby环境(安装参考:(24条消息) redis集群搭建(安装Ruby环境)redsi3.0.7 ruby2.3.1 centos6.5_快乐咸鱼Y的博客-CSDN博客_redis安装ruby)。

​ 在redis安装目录内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

​ 注意:

​ create --cluster-replicas是规定集群分配的方式 1为一台主服务器配置一台从服务器,平均分配 。

​ 上面的指令应为一行,三行写法只为阅读方便,192.168.11.101:6379除应填服务器真实地址,不能 填写localhost或者127.0.0.1,此处六台数据库基于一条服务器是因硬件条件限制,正常情况下应该 为六台不同服务器。

对集群的操作:

​ 每个redis集群有16384个插槽,他们的空间有N个主服务器平分,每台主服务器存储所有数据的n/1 的数据,每台主服务其都有自己的插槽段。

​ 在我们写入键值对时,redis会对我们写入的key使用CRC16(key)%算法来进行key的插槽值,然后再 交给相应的服务器管理。

redis集群错误处理:
当集群中每一台主服务器down机后,他的从服务器会反客为主取代其作为该插槽段的主服务器继续 提供服务。

​ 当redis集群中的某台主服务器和从服务器同时都down机后,服务器根据你的配置决定集群的其他插 槽段上的主服务器是否继续提供服务

​ 配置文件中的**cluster-require-full-coverage **值为yes时,当发生上述情况,整个redis集群都会直接 瘫痪,当值为no时,仅down机的那一个插槽段瘫痪,其他插槽段继续提供服务。

redis集群的优缺点:

​ 优点:(1)实现扩容 (2)分担压力 (3)无中心化操作相对简单

​ 缺点:(1)不支持多键操作 (2)不支持事务,不支持lua脚本

​ (3)方案出现较晚,公司数据迁移麻烦。

redis应用问题

缓存穿透

​ 定义:key对应的数据在数据库和缓存中并不存在,所有每次类似的请求均无法在缓存中获取,缓存 就会将请求返回给数据库,当大量的类似请求出现,每一次均越过缓存直接请求数据库就会造成数据 库的压力急剧加大,最终导致数据库崩溃,服务器也随之崩溃

​ 缓存穿透特征:服务器压力非正常的突然增大,redis命中率急剧下降,数据库查询量急剧增加

​ 解决方案:

​ (1)储存null值:对非法请求查询返回的空值进行缓存,并设置空值的缓存时间,缓存时间最长 不超过五分钟

​ (2)设置可访问名单:通过存入bitmap数据类型,将bitmap中的id作为数据偏移量,若查询的id 不在bitmap中,则进行拦 截,不允许访问。

​ (3)使用布隆过滤器,1970年布隆提出的,底层是一个巨大的二进制向量(位图)和一系列随机映射函数,是白名单方式的一个优 化版本,效率得到很大的提高

缓存击穿

​ 定义:当缓存中某一个key的值存在但处于过期状态,此时又存在瞬时的大量的对于这个key的请求,也是就是这个key是一个热点数据,此时因key已经过期,无法在缓存中获取此数据,所以大量的并发请求就去直接去请求数据库,就会造成是数据库压力瞬间增大,导致数据库崩溃。

​ 缓存穿透的特征:数据库压力瞬间增大,redis中并没有大量的key过期,redis压力并未增大而是正常运行

​ 解决方案:

​ (1)预先设置热门数据:预先存入热门数据相关的key,并将相关key时长加大

​ (2)实时监控:实时监控redis运行,判断出热门数据并加长他们的时长

​ (3)使用锁限制:当key过期后,并不第一时间去load db(加载数据库),而是先通过某一个线程去查询数据库,查询成功后将数· 据在redis中进刷新,redis的key刷新后再释放锁。

缓存雪崩

​ 定义:当短时间内缓存中大量的key同时过期,导致大量的请求均发送至数据库,数据库压力增大导致性能下降或者数据库崩溃。

​ 缓存雪崩的特征:数据库压力增大

​ 解决方案:

​ (1)多级缓存:整个运行服务中设置多级缓存(nginx缓存+reids缓存+其他缓存(ehcache缓存))

​ (2)使用锁或者队列:使用锁限制并发,放行单条请求查询数据库,其他线程的请求则进入等待,redis的key的内容刷新后,大量 数据查询的依旧是缓存。

​ (3)设置过期标志更新缓存:对缓存中的key设置提前量,提前量达到时就异步的去查询数据库,然后更新缓存

​ (4)将缓存失效时间分散开:通过随机数生成过期时间(一般范围在1-5分钟),这样一般就不会出现大量的key值同时过期。

分布式锁

​ 定义:在实际应用中,redis集群和数据库的一般都部署在多台服务器上,当我们在某一台服务上设置了锁,其他的服务器并不能获取这一台服务器设置的锁,也就是独立锁,这种锁只适合单机开发。而目前实际应用中我们需要设置的锁能被项目中的所有服务器读取 到,也就必须实现分布式锁(共享锁)。

​ redis实现分布式锁:

​ (1)通过setnx key value指令来设置分布式锁,用del key指令来释放分布式锁

​ (2)基本的设置方式若忘记释放锁则会使锁一直存在,其他限制无法访问,这并不符合开发需求。所以我们可以锁设置一个过期时 间,在设置完锁后,可以使用expire key time指令来给锁设置过期时间。

​ (3)设置锁与设置过期时间分开操作,在一些极端情况(服务器突然down机)时会有原子性问题,所以我们索性将锁和过期时间 一同设置,使用set key value nx ex来实现,这是最稳妥的一套方案。

​ uuid防止锁误释放:

public void redisLockTest(){
    //设置分布式锁,10秒时间锁自动释放
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111", 10, TimeUnit.SECONDS);
    //判断锁是否存在
    if(lock){
        //获取redis中key为num的值
        Object value = redisTemplate.opsForValue().get("num");
        //判断num的值是否为空
        if(StringUtils.isEmpty(value)){
            //若为空则直接推出方法
            return;
        }
        //不为空则将其转化为整数类型
        int num = Integer.parseInt(value+"");
        //给num++
        redisTemplate.opsForValue().set("num",++num);
        //释放锁
        redisTemplate.delete("lock");
    }

}

​ 锁误释放:上面的例子中,每次使用该方法我们都有两个释放锁的机制,第一个是设置锁时设置的锁的过期时间10秒,时间一到则锁自动过期,还有一个是代码最后的主动释放锁。正常情况下,我们的代码不会运行到锁过期的时候。但有一些特殊情况,当三个线程争夺该锁,我们称三个线程为a,b,c三个线程,若a先抢到锁,先执行里面的程序,但执行到一半因一些未知原因导致卡顿,一直卡顿到锁自动释放的时间服务器仍然未做出反应,10秒钟一到锁就会自动释放,而后b和c会去争夺这把锁,假设b拿到了,b的业务执行到一半,a线程反应过来,最后执行了redisTemplate.delete(“lock”);,就会将b获得的锁直接释放掉,这就导致原本是b拥有锁,但锁却被a释放掉,这个现象就是锁误释放,这严重违背开发原则。

​ 解决方案:UUID避免锁误释放:

public void redisLockTest(){
    //生成线程使用锁的唯一标识UUID
    String uuid = UUID.randomUUID().toString();
    //设置分布式锁,10秒时间锁自动释放,并将生成的UUID作为锁的值
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid , 10, TimeUnit.SECONDS);
    //判断锁是否存在
    if(lock){
        //获取redis中key为num的值
        Object value = redisTemplate.opsForValue().get("num");
        //判断num的值是否为空
        if(StringUtils.isEmpty(value)){
            //若为空则直接推出方法
            return;
        }
        //不为空则将其转化为整数类型
        int num = Integer.parseInt(value+"");
        //给num++
        redisTemplate.opsForValue().set("num",++num);
        //获取当前线程执行的锁的uuid
        String lockUuid = (String) redisTemplate.opsForValue().get("lock");
        //比较uuid是否相同,相同才释放,不相同就不释放
        if(lockUuid.equals(uuid)){
            //释放锁
            redisTemplate.delete("lock");
        }
        
    }

}

​ 上面的代码是经过优化后的代码:

​ 加入了以下几步:

​ 在每次调用该方法时都会获取不同的uuid(UUID算法生成)

​ 将该uuid作为锁的value值。

​ 在最后手动释放锁之前加上获取当前执行锁的uuid和该方法中的uuid做比较,相同才进行手动释放。

redis锁原子性问题:

​ 在经过上面的优化过后,能避免掉程序卡顿造成的常规的锁误释放,但因为上述代码还是分布进行的,所以并不具备原子性,在上述程序a线程刚好运行到手动释放这一步,就是把外层的if(lockUuid.equals(uuid))判定为真到手动释放锁这个过程,有个时间差,在这个时间差中可能正好锁的自动释放时间到了,锁被自动释放掉,且b线程抢到了这个锁,然后a线程又进行了锁的手动释放,就会将b刚获取到的锁释放掉,造成a线程释放b线程的锁的问题。

​ 要解决这类问题,我的方案是使用lua脚本来将所有操作统合在一起 执行,锁自动释放或者手动释放之后lua脚本失效。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值