1. redis相关概念

1. redis的事务

1.1 相关命令

MULTI:开启事务

EXEC:执行命令串

DISCARD:取消执行

WATCH:监视key

UNWATCH:取消监视key

1.2 解释

redis事务,就是将一系列命令加入到一个队列中,当执行exec的时候按照加入队列的顺序执行命令,当命令有误的时候,分为以下两种情况:

  1. 语法错误(编译器错误),此时会回滚
  2. redis类型错误(运行时错误),此时不会回滚,会跳过错误的命令继续向下执行

为什么Redis不支持事务回滚?因为多数事务失败是由于语法错误或者是数据类型错误导致的,语法错误时命令入队时就进行检测的,而类型错误是在命令执行的时候检测的;上述两种类型的错误是在上线之前就应该被发现的,Redis采用这种简单的事务来提高性能

watch和unwatch:

  1. 可以使用watch来监视一个或者多个key:开启监视后,若在事务执行之前修改了key的值,那么事务将会直接失败,完全进行回滚
  2. unwatch:取消监视

2. redis持久化机制

2.1 RDB-redis database

RDB是redis默认的持久化机制;RDB持久化文件是一个二进制文件,存储的是内存当中的数据

  1. RDB持久化的时机:
    1. 在配置文件中进行配置,save 900 1、save 300 10、save 60 10000;在900秒内有一个key改变了、300秒内10个、60秒内10000个key发生改变了,就执行持久化
    2. 关闭redis服务端的时候(shutdown命令),如果没有开启AOF持久化功能,那么会自动执行一次bgsave
    3. 主从同步(slave和master建立同步机制)
  2. 优缺点:
    1. 优点:RDB文件小,非常适合定时备份;redis加载RDB文件比AOF快很多,因为RDB文件存储的就是内存中的数据,AOF文件中存储的是一条条命令
    2. 缺点:RDB无法做到实时持久化,若在两次bgsave之间宕机,就会丢失部分数据;fork子进程进行cow操作的时候会阻塞主进程

save命令:会阻塞当前服务器,直到RDB完成为止,如果数据量大的话会造成长时间的阻塞,线上环境一般禁止使用 bgsave命令:就是background save,执行bgsave命令时Redis主进程会fork一个子进程来完成RDB的过程,完成后自动结束(操作系统的多进程Copy On Write机制,简称COW

2.2 AOF-append only file

AOF持久化默认关闭,需要手动开启;redis官方推荐同时使用RDB和AOF持久化,可以避免数据丢失;AOF文件是一个文本文件,存放的是命令

  1. AOF持久化时机:在配置文件中进行配置,appendsync always:没执行一个写操作,立即持久化到AOF文件中、appendsync everysec:每秒执行一次、appendsync no:由操作系统来判断什么时候执行

  2. 优缺点:

    1. 优点:不会阻塞进程;安全,使用everysec的选项最多只会丢失一秒的数据
    2. 缺点:
      1. AOF持久化日志文件很大
      2. 恢复的速度很慢
      3. 比较吃服务器的IO性能

推荐两种方式同时使用,在服务器关闭后恢复数据的时候建议使用AOF文件来恢复数据

3. redis集群配置

为了提高系统的高可用性,可以部署多台redis服务器

3.1 主从

Redis单节点性能是有上限的,为了提高性能,可以使用主从模式

主节点可以进行读、写的操作

从节点只能进行读操作

为了让主和从的数据保持一致,redis提供了replication机制,用来将主节点的数据同步到从节点

系统运行时,如果master挂掉了,可以在一个从库(如slave1)上手动执行命令slaveof no one,将slave1变成新的master;在slave2和slave3上分别执行slaveof 192.168.1.11 6379 将这两个机器的主节点指向的这个新的master;同时,挂掉的原master启动后作为新的slave也指向新的master上。

缺点

  1. 主节点宕机后,需要手动切换从节点为主节点;且会有一部分数据未能及时同步到从节点
  2. 如果多个从节点同时宕机需要重启,如果同时重启,为了同步数据会导致主节点IO剧增引起主节点宕机

3.2 哨兵

主从模式下,主节点宕机需手动切换从节点为主节点,造成一系列问题例如短时间内服务暂时无法使用,生产环境一般优先使用哨兵模式

哨兵模式

在主从模式下,redis同时提供了哨兵命令redis-sentinel,哨兵是一个独立的进程,原理是哨兵进程会向所有的redis主机发送命令并等待回应,监控运行的主机,哨兵可以有多个,多个哨兵之间也会通信,一般为了决策选举,会配置奇数个哨兵,当主节点宕机,会投票从slave选举一个主节点

这样子就避免了主从模式的切换主节点的缺点,提高了可用性

缺点

  1. 具有主从模式的缺点,且每台机器上面的数据是一致的,浪费了内存
  2. 难以支持在线扩容,管理复杂

3.3 集群

主从解决了读写压力大的问题、哨兵解决了主节点宕机后服务暂时不可用的问题;如果数据量非常大,就需要使用集群模式

集群模式:至少6个redis服务,三主三从,但是从节点不干活,只负责备份数据,当主节点挂了,就立刻顶上去

如果某个节点压力过大,可以针对该节点单独搭建读写分离的主从模式

详情:https://zhuanlan.zhihu.com/p/177000194

4. redis常见问题

1. key的生存时间到了之后会立即被删除吗

不会立即删除!!!

redis是单线程的,在执行删除指令的时候无法进行别的操作,影响性能;所以不会立即删除

删除机制:

  1. 定期删除:redis每隔一段时间去查看字典,字典中存放着设置了过期时间的key,每隔100ms查看一次,删除过期的key
  2. 惰性删除:当查询一个过了生存时间的key时,redis会查看当前key的生存时间;如果查询的key已经过了生存时间会返回一个null,同时删除这个key

无论redis有没有删除这个key,外界都查询不到!!! 只是没删的话会占用内存而已

2. redis的淘汰机制

在Redis内存已经满的时候,添加了一个新的数据,执行淘汰机制。

  • volatile-lru:在内存不足时,Redis会在设置过了生存时间的key中干掉一个最近最少使用的key。
  • allkeys-lru:在内存不足时,Redis会再全部的key中干掉一个最近最少使用的key。
  • volatile-lfu:在内存不足时,Redis会再设置过了生存时间的key中干掉一个最近最少频次使用的key。
  • allkeys-lfu:在内存不足时,Redis会再全部的key中干掉一个最近最少频次使用的key。
  • volatile-random:在内存不足时,Redis会再设置过了生存时间的key中随机干掉一个。
  • allkeys-random:在内存不足时,Redis会再全部的key中随机干掉一个。
  • volatile-ttl:在内存不足时,Redis会在设置过了生存时间的key中干掉一个剩余生存时间最少的key。
  • noeviction:(默认)在内存不足时,直接报错。

指定淘汰机制的方式:maxmemory-policy 具体策略,设置Redis的最大内存:maxmemory 字节大小

3. 缓存的常见问题

1. 缓存穿透

如果用户查询一个数据库中不存在的数据:

  1. 首先查redis,redis中没有
  2. 进入数据库,数据库中也没有
  3. 返回null,也不存入redis中;导致每次都是查数据库,redis起不到效果

如果这种请求比较多,或者用户故意使用这种请求进行恶意攻击,就会给数据库带来很大压力甚至崩溃,这种现象就是缓存穿透

解决办法

  1. 如果数据库中查不到就在redis中存一个null
  2. 使用布隆过滤器;布隆过滤器是一种算法,具体实现方式有很多种,例如使用RedisBloom去操作数据

缓存预热:在系统刚启动的时候,将一部分数据提前加载到redis中,这样子就可以避免在用户第一次请求的时候直接去访问数据库

Bloom过滤器:https://developer.aliyun.com/article/773205

2. 缓存击穿

指一个设置了过期时间的热点数据key,无时不刻在接收大量的请求,在一瞬间突然过期了,这个时候就要查询数据库了,导致数据库的压力瞬间增加,这种现象称为缓存击穿

解决办法

  1. 设置热点数据永不过期

  2. 使用分布式锁

    采用分布式锁的方式,限制请求进入数据库

    1. 加锁:当请求过来之后先进入缓存,缓存中没有就加分布式锁,第一个获取到锁的线程才能进入数据库,并将结果写回到缓存中
    2. 解锁:当其他进程发现锁已经被获取到之后就进入等待,知道解锁后,其他线程再依次访问被缓存的key

3. 缓存雪崩

缓存雪崩是缓存击穿的进阶版,当多个热点数据同时失效,大量请求同时进入数据库,导致数据库压力瞬间增大,导致宕机

解决方案

  1. 设置key的过期时间的时候,使用随机数的方式,避免大量key同时过期
  2. 热点数据不设置过期时间

4. 缓存倾斜

某个key存放的是一个普通热点数据,但是突然访问量暴增导致该redis节点挂了然后请求又到达下一个服务器,下一个服务器又承受不了崩掉,最终导致所有的缓存服务器崩掉,这就是缓存倾斜

举个例子,存放林志玲信息的节点在林志玲结婚那天访问暴增,导致该节点崩了,然后剩下的一个个也崩了

解决方案

  1. 将一些特别热点的key直接放在客户端进行存储,设置过期时间,过期后再到后台进行查询
  2. 可以将这个key复制出一些子key,这些子key有相同的value值,查询的时候使用hash取模算法,将压力分摊到不同的节点

reids相关名词可见:https://blog.csdn.net/qq_44750696/article/details/103883241

5. Jeids和RedisTemplate的区别

https://blog.csdn.net/csdn2497242041/article/details/102675435

6. Redis的管道操作

redis的每次操作都到等待请求,中间网络耗时是非常高的,我们可以将命令给打包到一个管道Pipeline中,一次性发送给redis,Redis再将所有的结果一次性返回

//  Redis管道的操作
@Test
public void pipeline(){
    //1. 创建连接池
    JedisPool pool = new JedisPool("192.168.199.109",6379);
    long l = System.currentTimeMillis();

    /*//2. 获取一个连接对象
    Jedis jedis = pool.getResource();

    //3. 执行incr - 100000次
    for (int i = 0; i < 100000; i++) {
        jedis.incr("pp");
    }

    //4. 释放资源
    jedis.close();*/

    //================================
    //2. 获取一个连接对象
    Jedis jedis = pool.getResource();
    //3. 创建管道
    Pipeline pipelined = jedis.pipelined();
    //3. 执行incr - 100000次放到管道中
    for (int i = 0; i < 100000; i++) {
        pipelined.incr("qq");
    }
    //4. 执行命令
    pipelined.syncAndReturnAll();
    //5. 释放资源
    jedis.close();

    System.out.println(System.currentTimeMillis() - l);
}

7. Redis实现分布式锁

有两种方案,一种是基于Redis命令;一种是基于Redis Lua脚本

基于Redis命令

  1. 加锁:执行setnx,若执行成功再设置超时时间;
  2. 解锁:执行delete命令
  3. 优点:实现简单,较为轻便
  4. 缺点:setnx和expire分两步执行,非原子操作,可能会造成死锁;不支持阻塞等待,不可重入

基于redis Lua脚本

  1. 加锁:执行SET lock_name random_value EX seconds NX 命令
  2. 解锁:执行脚本
  3. 优点:逻辑严谨
  4. 缺点:不支持阻塞等待、不可重入

上面的只是思想,实际项目中我们可以使用封装好的框架,例如Redisson

代码示例

//加锁设置的参数
@Data
public class LockParam {
    //锁的key
    private String lockKey;
    //尝试获得锁的时间(单位:毫秒),默认值:3000毫秒
    private Long tryLockTime;
    //尝试获得锁后,持有锁的时间(单位:毫秒),默认值:5000毫秒
    private Long holdLockTime;

    public LockParam(String lockKey){
        this(lockKey,1000*3L,1000*5L);
    };
    public LockParam(String lockKey,Long tryLockTime){
        this(lockKey,tryLockTime,1000*5L);
    };
    public LockParam(String lockKey,Long tryLockTime,Long holdLockTime){
        this.lockKey = lockKey;
        this.tryLockTime = tryLockTime;
        this.holdLockTime = holdLockTime;
    };
}
/**
 * redis分布式锁
 */
@Slf4j
public class RedisLock {

    //锁key的前缀
    private final static String prefix_key = "redisLock:";
    //释放锁的lua脚本
    private final static String unLockScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    //执行unLockScript脚本,释放锁成功值
    private final static Long unLockSuccess = 1L;


    //加锁设置的参数(key值、超时时间、持有锁的时间)
    private LockParam lockParam;
    //尝试获得锁的截止时间【lockParam.getTryLockTime()+System.currentTimeMillis()】
    private Long tryLockEndTime; // 当前时间+超时时间
    //redis加锁的key
    private String redisLockKey;
    //redis加锁的vlaus
    private String redisLockValue;
    //redis加锁的成功标示
    private Boolean holdLockSuccess = Boolean.FALSE;


    //jedis实例
    private Jedis jedis;

    //获取jedis实例
    private Jedis getJedis() {
        return this.jedis;
    }

    //关闭jedis
    private void closeJedis(Jedis jedis) {
        jedis.close();
        jedis = null;
    }

    public RedisLock(LockParam lockParam) {
        if (lockParam == null) {
            new RuntimeException("lockParam is null");
        }
        if (lockParam.getLockKey() == null || lockParam.getLockKey().trim().length() == 0) {
            new RuntimeException("lockParam lockKey is error");
        }
        this.lockParam = lockParam;

        this.tryLockEndTime = lockParam.getTryLockTime() + System.currentTimeMillis();
        this.redisLockKey = prefix_key.concat(lockParam.getLockKey());
        this.redisLockValue = UUID.randomUUID().toString().replaceAll("-", "");

        //todo 到时候可以更换获取Jedis实例的实现
        jedis = new Jedis("120.26.63.19", 6379);
        jedis.auth("12345678");
    }

    /**
     * 加锁
     *
     * @return 成功返回true,失败返回false
     */
    public boolean lock() {
        while (true) {
            //判断是否超过了,尝试获取锁的时间
            if (System.currentTimeMillis() > tryLockEndTime) {
                return false;
            }
            //尝试获取锁
            holdLockSuccess = tryLock();
            if (Boolean.TRUE.equals(holdLockSuccess)) {
                return true;//获取锁成功
            }

            try {
                //获得锁失败,休眠50毫秒再去尝试获得锁,避免一直请求redis,导致redis cpu飙升
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 执行一次加锁操作:成功返回true 失败返回false
     *
     * @return 成功返回true,失败返回false
     */
    private boolean tryLock() {
        try {
            String result = getJedis().set(redisLockKey, redisLockValue, "NX", "PX", lockParam.getHoldLockTime());
            if ("OK".equals(result)) {
                return true;
            }
        } catch (Exception e) {
            log.warn("tryLock failure redisLockKey:{} redisLockValue:{} lockParam:{}", redisLockKey, redisLockValue, lockParam, e);
        }
        return false;
    }

    /**
     * 解锁
     *
     * @return 成功返回true,失败返回false
     */
    public Boolean unlock() {
        Object result = null;
        try {
            //获得锁成功,才执行lua脚本
            if (Boolean.TRUE.equals(holdLockSuccess)) {
                //执行Lua脚本
                result = getJedis().eval(unLockScript, Collections.singletonList(redisLockKey), Collections.singletonList(redisLockValue));
                if (unLockSuccess.equals(result)) {//释放成功
                    return true;
                }
            }
        } catch (Exception e) {
            log.warn("unlock failure redisLockKey:{} redisLockValue:{} lockParam:{} result:{}", redisLockKey, redisLockValue, lockParam, result, e);
        } finally {
            this.closeJedis(jedis);
        }
        return false;
    }
}
@Slf4j
public class test {
    static String lockKey = "666";
    public static void main(String[] args) throws InterruptedException {
        log.info("下面测试两个线程同时,抢占锁的结果");
        Thread thread1 = new Thread(()->{
            testRedisLock();
        });
        thread1.setName("我是线程1");
        Thread thread2 = new Thread(()->{
            testRedisLock();
        });
        thread2.setName("我是线程2");

        //同时启动线程
        thread1.start();
        thread2.start();

        Thread.sleep(1000*20);
        log.info("-----------------我是一条分割线----------------");
        log.info("");
        log.info("");
        log.info("");


        log.info("下面是测试  一个线程获取锁成功后,由于业务执行时间超过了设置持有锁的时间,是否会把其他线程持有的锁给释放掉");
        Thread thread3 = new Thread(()->{
            testRedisLock2();
        });
        thread3.setName("我是线程3");
        thread3.start();

        Thread.sleep(1000*1);//暂停一秒是为了让线程3获的到锁
        Thread thread4 = new Thread(()->{
            testRedisLock();
        });
        thread4.setName("我是线程4");
        thread4.start();
    }

    public static void testRedisLock(){
        LockParam lockParam = new LockParam(lockKey);
        lockParam.setTryLockTime(2000L);//2秒时间尝试获得锁
        lockParam.setHoldLockTime(1000*10L);//获得锁成功后持有锁10秒时间
        RedisLock redisLock = new RedisLock(lockParam);
        try {
            Boolean lockFlag = redisLock.lock();
            log.info("加锁结果:{}" + System.currentTimeMillis(),lockFlag);
            if(lockFlag){
                try {
                    //20秒模拟处理业务代码时间
                    Thread.sleep(1000*5L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }catch (Exception e) {
            log.info("testRedisLock e---->",e);
        }finally {
            boolean unlockResp = redisLock.unlock();
            log.info("释放锁结果:{}",unlockResp);
        }
    }


    public static void testRedisLock2(){
        LockParam lockParam = new LockParam(lockKey);
        lockParam.setTryLockTime(1000*2L);//2秒时间尝试获得锁
        lockParam.setHoldLockTime(1000*2L);//获得锁成功后持有锁2秒时间
        RedisLock redisLock = new RedisLock(lockParam);
        try {
            Boolean lockFlag = redisLock.lock();
            log.info("加锁结果:{}" + System.currentTimeMillis(),lockFlag);
            if(lockFlag){
                try {
                    //10秒模拟处理业务代码时间
                    Thread.sleep(1000*10L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }catch (Exception e) {
            log.info("testRedisLock e---->",e);
        }finally {
            boolean unlockResp = redisLock.unlock();
            log.info("释放锁结果:{}",unlockResp);
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值