Redis高级用法

522 阅读15分钟

Redis高级

1.redis介绍,为啥不用MySql、Zookeeper来实现分布式锁。

注意:命令不区分大小写,而key是区分大小写的。想查看某一个数据类型的操作指令,可以用help @类型名词指令

2.redis传统五大基本类型的落地应用

String

  • 基础命令
set key value
get key
同时设置/获取多个键值

MSET key value [key value ....]
MGET key [key ....]
  • 数值增减
递增数字
INCR key
增加指定的整数
INCRBY key increment
递减数值
DECR key
减少指定的整数
DECRBY key decrement
减少指定的整数
STRLEN key

应用场景

  • 商品编号、订单号采用INCR命令生成
  • 平台的点赞和踩,点赞递增,踩递减 image.png 1.png
  • 分布式锁
setnx key value
set key value [Ex seconds][PX milliseconds][NX|XX]
  • EX:key在多少秒之后过期
  • PX:key在多少毫秒之后过期
  • NX:当key不存在的时候,才创建key,效果等同于setnx
  • XX:当key存在的时候,覆盖key

hash

redis的哈希结果形如Map<String,Map<Object,object>>

一次设置一个字段值 
HSET key field value
一次获取一个字段值 
HGET key field
一次设置多个字段值 
HMSET key field value [field value …]
一次获取多个字段值 
HMGET key field [field …]
获取所有字段值 
HGETALL key
获取某个key内的全部数量 
HLEN
删除一个
key HDEL

应用场景

2.png 购物车早期,当前小中厂可用

  • 新增商品 hset shopcar:uid1024 334488 1
  • 新增商品 hset shopcar:uid1024 334477 1
  • 增加商品数量 hincrby shopcar:uid1024 334477 1
  • 商品总数 hlen shopcar:uid1024
  • 全部选择 hgetall shopcar:uid1024

list

向列表左边添加元素 
LPUSH key value [value …]
向列表右边添加元素 
RPUSH key value [value …]
查看列表 
LRANGE key start stop
获取列表中元素的个数 
LLEN key

应用场景 - 微信文章订阅公众号

  • 大V作者李永乐老师和掘金官方发布了文章分别是1122
  • 阳哥关注了他们两个,只要他们发布了新文章,就会安装进我的List: lpush likearticle:阳哥id1122
  • 查看阳哥自己的号订阅的全部文章,类似分页,下面0~10就是一次显示10条 lrange likearticle:阳哥id 0 10

set

语法

添加元素 
SADD key member [member …]
删除元素 
SREM key member [member …]
获取集合中的所有元素 
SMEMBERS key
判断元素是否在集合中 
SISMEMBER key member
获取集合中的元素个数 
SCARD key
从集合中随机弹出一个元素,元素不删除 
SRANDMEMBER key [数字]
从集合中随机弹出一个元素,出一个删一个 
SPOP key[数字]

集合运算

  • 集合的差集运算A - B
    • 属于A但不属于B的元素构成的集合
    • SDIFF key [key …]
  • 集合的交集运算A ∩ B
    • 属于A同时也属于B的共同拥有的元素构成的集合
    • SINTER key [key …]
  • 集合的并集运算A U B
    • 属于A或者属于B的元素合并后的集合
    • SUNION key [key …]

应用场景

微信抽奖小程序

image.png

  • 用户ID,立即参与按钮
    • SADD key 用户ID
  • 显示已经有多少人参与了、上图466人参加
    • SCARD key
  • 抽奖(从set中任意选取N个中奖人)
    • SRANDMEMBER key 2(随机抽奖2个人,元素不删除)
    • SPOP key 3(随机抽奖3个人,元素会删除)

微信朋友圈点赞

  • 新增点赞
    • sadd pub:msglD 点赞用户ID1 点赞用户ID2
  • 取消点赞
    • srem pub:msglD 点赞用户ID
  • 展现所有点赞过的用户
    • SMEMBERS pub:msglD
  • 点赞用户数统计,就是常见的点赞红色数字
    • scard pub:msgID
  • 判断某个朋友是否对楼主点赞过
    • SISMEMBER pub:msglD 用户ID

微博好友关注社交关系

3.png

  • 共同关注:我去到局座张召忠的微博,马上获得我和局座共同关注的人
    • sadd s1 1 2 3 4 5
    • sadd s2 3 4 5 6 7
    • SINTER s1 s2
    • 我关注的人也关注他(大家爱好相同)
  • QQ内推可能认识的人
    • sadd s1 1 2 3 4 5
    • sadd s2 3 4 5 6 7
    • SINTER s1 s2
    • SDIFF s1 s2
    • SDIFF s2 s1

zet

向有序集合中加入一个元素和该元素的分数

添加元素 
ZADD key score member [score member …]
按照元素分数从小到大的顺序返回索引从start到stop之间的所有元素 
ZRANGE key start stop [WITHSCORES]
获取元素的分数 
ZSCORE key member
删除元素 
ZREM key member [member …]
获取指定分数范围的元素 
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
增加某个元素的分数 
ZINCRBY key increment member
获取集合中元素的数量 
ZCARD key
获得指定分数范围内的元素个数 
ZCOUNT key min max
按照排名范围删除元素 
ZREMRANGEBYRANK key start stop
  • 获取元素的排名
从小到大 
ZRANK key member
从大到小 
ZREVRANK key member

应用场景

根据商品销售对商品进行排序显示

  • 定义商品销售排行榜(sorted set集合),key为goods:sellsort,分数为商品销售数量。
  • 商品编号1001的销量是9,商品编号1002的销量是15
    • zadd goods:sellsort 9 1001 15 1002
  • 有一个客户又买了2件商品1001,商品编号1001销量加2
    • zincrby goods:sellsort 2 1001
  • 求商品销量前10名
    • ZRANGE goods:sellsort 0 10 withscores 抖音热搜
  • 点击视频
    • ZINCRBY hotvcr:20200919 1 八佰
    • ZINCRBY hotvcr:20200919 15 八佰 2 花木兰
  • 展示当日排行前10
    • ZREVRANGE hotvcr:20200919 0 9 withscores

3.知道分布式锁吗?有哪些实现方案? 你谈谈对redis分布式锁的理解, 删key的时候有什么问题?

面试真题

  • Redis除了拿来做缓存,你还见过基于Redis的什么用法?
  • Redis做分布式锁的时候有需要注意的问题
  • 如果是Redis是单点部署的,会带来什么问题?那你准备怎么解决单点问题呢?
  • 集群模式下,比如主从模式,有没有什么问题呢?
  • 那你简单的介绍一下Redlock吧?你简历上写redisson,你谈谈
  • Redis分布式锁如何续期?看门狗知道吗?

先来一个类似抢票的小案例。

新建模块

6.png pom文件

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.3.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
 <dependencies>

        <!-- web+actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

        <!-- SpringBoot与Redis整合依赖 -->

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

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!-- jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>

        <!-- Spring Boot AOP技术-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>

        <!-- 一般通用基础配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

配置redis

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory){
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return redisTemplate;
    }
}

配置

server.port=1111
#2222

#=========================redis相关配香========================
#Redis数据库索引(默认方0)
spring.redis.database=0
#Redis服务器地址
spring.redis.host=127.0.0.1
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码(默认为空)
spring.redis.password=
#连接池最大连接数(使用负值表示没有限制)默认8
spring.redis.lettuce.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)默认-1
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接默认8
spring.redis.lettuce.pool.max-idle=8
#连接池中的最小空闲连接默犬认0
spring.redis.lettuce.pool.min-idle=0

业务类

@RestController
public class GoodController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buy_goods")
    public String buy_Goods(){

        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);

        if (goodsNumber > 0){
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort);
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
        }else {
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);
        }
        return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
    }

}

分别启动项目进行测试,

7.png 单机下并没有报错,下面将他配置到分布式环境中,看需要解决哪些问题。

1、没有加锁,并发下数字不对,出现超卖现象

在单机环境下,可以使用synchronizedLock来实现。

private final Lock lock = new ReentrantLock();
        lock.lock();  // block until condition holds//不见不散
        try {
            // ... method body
        } finally {
            lock.unlock()
        }
        
        
       	if(lock.tryLock(timeout, unit)){//过时不候
            try {
            // ... method body
            } finally {
                lock.unlock()
            }   
        }else{
            // perform alternative actions
        }

但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现,比如redis或者zookeeper来构建;

不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程.

2、nginx分布式微服务架构

分布式部署后,单机锁还是出现超卖现象,需要分布式锁

redis集群.png

Nginx配置负载均衡

  • 启动Nginx并测试通过,采用轮询的方式访问这两个项目。

nginx.png

  • 手动调用并不会出现错误,但在压测工具Jmeter帮助下,出现如下错误。

压测.png

这就是所谓分布式部署后出现超卖现象。分布式环境下,两个项目可能同时进入这把锁,所以我们得引入分布式锁。

Redis具有极高的性能,且其命令对分布式锁支持友好,借助SET命令即可实现加锁处理。

public static final String REDIS_LOCK = "hong";
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);

if(!flag) {
    return "抢锁失败";
}
// 逻辑代码
// 解锁
stringRedisTemplate.delete(REDIS_LOCK);

加锁解锁,lock/unlock必须同时出现并保证调用

3、出异常的话,可能无法释放锁, 必须要在代码层面finally释放锁

  String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    try {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
        
        if(!flag) {
        return "抢锁失败";
        }       
        ...//业务逻辑            
    }finally{
        stringRedisTemplate.delete(REDIS_LOCK);   
    }

4、宕机了

部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key

Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
//设定过期时间
stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);

5、设置key+过期时间分开了,必须要合并成一行具备原子性

使用自带方法,此方法保证了原子性

Boolean flag = stringRedisTemplate.opsForValue()//使用另一个带有设置超时操作的方法
        .setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);

6、张冠李戴,删除了别人的锁

业务执行超时,锁过期了,就可能会误删别人的锁。

4.png 只能自己删除自己的,不许动别人的,自己的UUID才能删。

finally{
        if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equals(value)) {
            stringRedisTemplate.delete(REDIS_LOCK);
        }
    }

7、finally块的判断+del删除操作不是原子性的

用redis自身的事务

事务介绍
  • Redis的事条是通过MULTIEXECDISCARDWATCH这四个命令来完成。
  • Redis的单个命令都是原子性的,所以这里确保事务性的对象是命令集合
  • Redis将命令集合序列化并确保处于一事务的命令集合连续且不被打断的执行。
  • Redis不支持回滚的操作。
命令描述
DISCARD取消事务,放弃执行事务块内的所有命令。
EXEC执行所有事务块内的命令。
MULTI标记一个事务块的开始。
UNWATCH取消 WATCH 命令对所有 key 的监视。
WATCH key [key …]监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

事务.png

如果在没执行EXEC的情况下开另一线程改变k1的值,那么事务就会报错。

事务2.png

finally {
            // 解锁
            // 类似与CAS逻辑,用事务来控制
            while (true) {
                stringRedisTemplate.watch(REDIS_LOCK); //加事务,乐观锁
                if (value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK))) {
                    stringRedisTemplate.setEnableTransactionSupport(true);
                    stringRedisTemplate.multi();//开始事务
                    stringRedisTemplate.delete(REDIS_LOCK);
                    List list = stringRedisTemplate.exec();
                    if (list == null) {  //如果等于null,就是没有删掉,删除失败,再回去while循环那再重新执行删除
                        continue;
                    }
                }
                //如果删除成功,释放监控器,并且breank跳出当前循环
                stringRedisTemplate.unwatch();
                break;
            }
     }

用Lua脚本

Redis调用Lua脚本通过eval命令保证代码执行的原子性

配置Jedis

public class RedisUtils {
    private static JedisPool jedisPool;

    static {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(20);
        jedisPoolConfig.setMaxIdle(10);

        jedisPool = new JedisPool(jedisPoolConfig,"ip",6379,100000);
    }

    public static Jedis getJedis() throws Exception {
        if (null != jedisPool){
            return jedisPool.getResource();
        }
        throw new Exception("Jedispool is not ok");
    }
}
            // 2、用lua脚本
            Jedis jedis = RedisUtils.getJedis();

            String script = "if redis.call('get', KEYS[1]) == ARGV[1]"+"then "
                    +"return redis.call('del', KEYS[1])"+"else "+ "  return 0 " + "end";
            try{
                Object result = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));
                if ("1".equals(result.toString())){
                    System.out.println("------del REDIS_LOCK_KEY success");
                }else {
                    System.out.println("------del REDIS_LOCK_KEY error");
                }
            }finally {
                if (null != jedis){
                    jedis.close();
                }
            }

8、确保redisLock过期时间大于业务执行时间的问题

Redis分布式锁如何续期?

回忆下CAP

  • C:Consistency(强一致性)
  • A:Availability(可用性)
  • P:Partition tolerance(分区容错性)

5.png 集群 + CAP对比ZooKeeper 对比ZooKeeper

  • redis主机收到信息,还没同步到slave从节点,就返回。
  • Redis
    • AP -redis异步复制造成的锁丢失,比如:主节点没来的及把刚刚set进来这条数据给从节点,就挂了。

ZooKeeper - CP

  • ZooKeeper主机收到信息,先通知从节点全部确认收到后才返回。

所以,Redis集群环境下,我们自己写的也不OK,直接上RedLockRedisson落地实现。

配置类新添

@Bean
public Redisson redisson(){
    Config config = new Config();
    config.useSingleServer().setAddress("redis://"+"127.0.0.1"+":6379").setDatabase(0);
    return (Redisson) Redisson.create(config);
}

引入Redisson

@Autowired private Redisson redisson;
RLock redissonLock = redisson.getLock(REDIS_LOCK); redissonLock.lock(); 
try {
    // 业务代码
} finally { 
    redissonLock.unlock(); 
}

在更大的高并发下可能报如下错误,为了更加严谨,我们采用下面的写法。

image.png

@RestController
public class GoodController {

    public static final String REDIS_LOCK = "hong";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String serverPort;

    @Autowired
    private Redisson redisson;

    @GetMapping("/buy_goods")
    public String buy_Goods() throws Exception {

        RLock redissonLock = redisson.getLock(REDIS_LOCK);
        redissonLock.lock();

        try {
            Boolean flag = stringRedisTemplate.opsForValue()//使用另一个带有设置超时操作的方法
                    .setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);

//            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
//            //设定时间
//            stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);

            if (!flag) {
                return "抢锁失败";
            }

            String result = stringRedisTemplate.opsForValue().get("goods:001");// get key ====看看库存的数量够不够
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if (goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
                System.out.println("成功买到商品,库存还剩下: " + realNumber + " 件" + "\t服务提供端口" + serverPort);

                return "成功买到商品,库存还剩下:" + realNumber + " 件" + "\t服务提供端口" + serverPort;
            } else {
                System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
            }
            return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
        } finally {
            // 解锁
            //添加后,更保险
            if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
                redissonLock.unlock();
            }
        }
    }
}

Redis分布锁总结

  • synchronized单机版oK,上分布式

  • nginx分布式微服务单机锁不行

  • 取消单机锁,上Redis分布式锁setnx

  • 只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁

  • 宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定

  • redis的分布式锁key,增加过期时间,此外,还必须要setnx+过期时间必须同一行

  • 必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3

  • Redis集群环境下,我们自己写的也不oK直接上RedLockRedisson落地实现

4.redis缓存过期淘汰策略

相关面试题

  • 生产上你们你们的redis内存设置多少?
  • 如何配置、修改redis的内存大小
  • 如果内存满了你怎么办?
  • redis清理内存的方式?定期删除和惰性删除了解过吗
  • redis缓存淘汰策略
  • redis的LRU了解过吗?可否手写一个LRU算法

Redis内存满了怎么办?Redis默认内存多少?在哪里查看?如何设置修改?

查看Redis最大占用内存

配置文件redis.conf的maxmemory参数,maxmemory是bytes字节类型,注意转换。

redis默认内存多少可以用?

如果不设置最大内存大小或者设置最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存

一般生产上你如何配置?

一般推荐Redis设置内存为最大物理内存的四分之三。

如何修改redis内存设置

  • 修改配置文件redis.confmaxmemory参数,如:maxmemory 104857600

  • 通过命令修改

    • config set maxmemory 1024
    • config get maxmemory

什么命令查看redis内存使用情况?

info memory

真要打满了会怎么样? 如果Redis内存使用超出了设置的最大值会怎样?

改改配置,故意把最大值设为1个byte试试

127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "0"
127.0.0.1:6379> config set maxmemory 1
OK
127.0.0.1:6379> set a 123
(error) OOM command not allowed when used memory > 'maxmemory'.

结论

  • 设置了maxmemory的选项,假如redis内存使用达到上限
  • 没有加上过期时间就会导致数据写满maxmemory 为了避免类似情况,引出下一章内存淘汰策略

redis缓存淘汰策略

淘汰策略.png

redis过期键的删除策略

如果一个键是过期的,那它到了过期时间之后是不是马上就从内存中被被删除呢?

如果不是,那过期后到底什么时候被删除呢??是个什么操作?

三种不同的删除策略

定期删除

Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。

立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,让CPU心累,时时需要删除,忙死。。。。。。

这会产生大量的性能消耗,同时也会影响数据的读取操作。

总结:对CPU不友好,用处理器性能换取储存空间(拿时间换空间)

惰性删除
  • 数据到达过期时间,不做处理。等下次访问该数据时,

  • 如果未过期,返回数据;

  • 发现已过期,删除,返回不存在。

  • 惰性删除策略的缺点是,它对内存是最不友好的

  • 如果一个键已经过期,而这个键又仍然保留在数据库中,那么只要这个过期键不被删除,它所占用的内存就不会释放。

  • 在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏–无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖于内存的Redis服务器来说,肯定不是一个好消息

总结:对memory不友好,用存储空间换取处理器性能(拿空间换时间)

上面两种方案都走极端——定期删除

定期删除策略是前两种策略的折中:

定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。

周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度

  • 特点1:CPU性能占用设置有峰值,检测频度可自定义设置
  • 特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理

总结:周期性抽查存储空间(随机抽查,重点抽查)

举例: redis默认每个100ms检查,是否有过期的key,有过期key则删除。注意:redis不是每隔100ms将所有的key检查一次而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis直接进去ICU)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。

定期删除策略的难点是确定删除操作执行的时长和频率:如果删除操作执行得太频繁,或者执行的时间太长,定期删除策略就会退化成定时删除策略,以至于将CPU时间过多地消耗在删除过期键上面。如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除束略一样,出现浪费内存的情况。因此,如果采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作的执行时长和执行频率

漏洞:1. 定期删除时,从来没有被抽查到:2. 惰性删除时,也从来没有被点中使用过 大量过期的key堆积在内存中,导致redis内存空间紧张或者很快耗尽

必须要有一个更好的兜底方案......这时候就要用到内存淘汰策略了。

内存淘汰策略(redis6.0.8版本)

  • noeviction:不会驱逐任何key
  • volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除
  • volatile-Iru:对所有设置了过期时间的key使用LRU算法进行删除
  • volatile-random:对所有设置了过期时间的key随机删除
  • volatile-ttl:删除马上要过期的key
  • allkeys-lfu:对所有key使用LFU算法进行删除
  • allkeys-Iru:对所有key使用LRU算法进行删除
  • allkeys-random:对所有key随机删除

快速记忆

  • 2个维度

    • 过期键中筛选
    • 所有键中筛选
  • 4个方面

    • LRU
    • LFU
    • random
    • ttl(Time To Live) 修改配置
  • 命令

    • config set maxmemory-policy noeviction
    • config get maxmemory
  • 配置文件 - 配置文件redis.confmaxmemory-policy参数

5.redis的LRU算法简介

LRULeast Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰。

  • 1 所谓缓存,必须要有读+写两个操作,按照命中率的思路考虑,写操作+读操作时间复杂度都需要为O(1)

  • 2 特性要求分析

    - 2.1 必须有顺序之分,以区分最近使用的和很久没用到的数据排序。

    - 2.2 写和读操作 一次搞定。

    - 2.3 如果容量(坑位)满了要删除最不长用的数据,每次新访问还要把新的数据插入到队头(按照业务你自己设定左右那一边是队头) 

  • 查找快,插入快,删除快,且还需要先后排序-------->什么样的数据结构满足这个问题?

  • 你是否可以在O(1)时间复杂度内完成这两种操作?

  • 如果一次就可以找到,你觉得什么数据结构最合适?

LRU的算法核心是哈希链表

本质就是HashMap+DoubleLinkedList 时间复杂度是O(1),哈希表+双向链表的结合体

编码手写如何实现LRU

参考LinkedHashMap(依赖JDK)

public class LRUCacheDemo<K, V> extends LinkedHashMap<K, V> {

    private int capacity;//缓存坑位

    public LRUCacheDemo(int capacity) {
        super(capacity, 0.75F, false);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return super.size() > capacity;
    }

    public static void main(String[] args) {
        LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3);

        lruCacheDemo.put(1, "a");
        lruCacheDemo.put(2, "b");
        lruCacheDemo.put(3, "c");
        System.out.println(lruCacheDemo.keySet());

        lruCacheDemo.put(4, "d");
        System.out.println(lruCacheDemo.keySet());

        lruCacheDemo.put(3, "c");
        System.out.println(lruCacheDemo.keySet());
        lruCacheDemo.put(3, "c");
        System.out.println(lruCacheDemo.keySet());
        lruCacheDemo.put(3, "c");
        System.out.println(lruCacheDemo.keySet());
        lruCacheDemo.put(5, "x");
        System.out.println(lruCacheDemo.keySet());
    }
}

不依赖JDK

class LRUCache2{
	class Node<K, V>{//双向链表节点
		K key;
		V value;
		Node<K, V> prev;
		Node<K, V> next;
		
		public Node() {
			this.prev = this.next = null;
		}
		public Node(K key, V value) {
			super();
			this.key = key;
			this.value = value;
		}
	}
	
    //新的插入头部,旧的从尾部移除
	class DoublyLinkedList<K, V>{
		Node<K, V> head;
		Node<K, V> tail;
		
		public DoublyLinkedList() {
            //头尾哨兵节点
			this.head = new Node<K, V>();
			this.tail = new Node<K, V>();
			this.head.next = this.tail;
			this.tail.prev = this.head;
		}
		
		public void addHead(Node<K, V> node) {
			node.next = this.head.next;
			node.prev = this.head;
			this.head.next.prev = node;
			this.head.next = node;
		}
		
		public void removeNode(Node<K, V> node) {
			node.prev.next = node.next;
			node.next.prev = node.prev;
			node.prev = null;
			node.next = null;

		}
		
		public Node<K, V> getLast() {
			if(this.tail.prev == this.head)
				return null;
			return this.tail.prev;
		}

	}
	
	private int cacheSize;
	private Map<Integer, Node<Integer, Integer>> map;
	private DoublyLinkedList<Integer, Integer> doublyLinkedList;
	
	
	public LRUCache2(int cacheSize) {
		this.cacheSize = cacheSize;
		map = new HashMap<>();
		doublyLinkedList = new DoublyLinkedList<>();
	}

	public int get(int key) {
		if(!map.containsKey(key)) {
			return -1;
		}
		
		Node<Integer, Integer> node = map.get(key);
        
        //更新节点位置,将节点移置链表头
		doublyLinkedList.removeNode(node);
		doublyLinkedList.addHead(node);
		
		return node.value;
	}
	
	public void put(int key, int value) {
		
		if(map.containsKey(key)) {
			
			Node<Integer, Integer> node = map.get(key);
			node.value = value;
			map.put(key, node);
			
            
			doublyLinkedList.removeNode(node);
			doublyLinkedList.addHead(node);
		}else {
			
			if(map.size() == cacheSize) {//已达到最大容量了,把旧的移除,让新的进来
				Node<Integer, Integer> lastNode = doublyLinkedList.getLast();
				map.remove(lastNode.key);//node.key主要用处,反向连接map
				doublyLinkedList.removeNode(lastNode);
			}
			
			Node<Integer, Integer> newNode = new Node<>(key, value);
			map.put(key, newNode);
			doublyLinkedList.addHead(newNode);
		}
	}	
}