【2020尚硅谷Java大厂面试题第三季 04】Redis 9种数据类型使用场景,分布式锁演变步骤,lua脚本,redis事务,Redisson,Redis内存占用,删除策略,内存淘汰策略,手写LRU

1.安装redis6.0.8

  • 2023 02 02 为:redis-7.0.8.tar.gz

2.redis传统五大数据类型的落地应用

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

4.redis缓存过期淘汰策略

  • 定期删除
  • 惰性删除

5.redis的LRu算法简介

Redis简介

https://redis.io/download/

http://www.redis.cn/ 中文地址

安全Bug按照官网提示,升级成为6.0.8

不过,就在前两天,Redis突然发布了紧急版本6.0.8,之前消息称6.0.7被称作最后一个6.x版本,但Redis 团队表示6.0.8版本升级迫切性等级为高:任何将Redis 6.0.7与Sentinel或CONFIG REWRITE命令配合使用的人都会受到影响,应尽快升级。

查看版本

redis-server -v
Redis server v=5.0.8 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=8d5f0851db110347


redis-cli
127.0.0.1:6379> info
# Server
redis_version:5.0.8

redis的数据类型 9种

  • String 字符类型

  • List 秒杀,先进先出。模拟栈。列表类型

    • l和rpush
    • l和rpop
  • Set 求交并差 集。集合类型。

  • Zset 根据分数排序。有序集合类型。SortedSet

  • Hash 存用户,更新用户,只取用户的某一个字段。散列类型

  • geo 地理空间

  • hyperloglog 求基数,两个合并后,去掉重复的元素。统计。

  • bitmaps 位图,每周哪天打卡记作1

  • RedisPub/Sub发布订阅,是Redis一步步完善消息队列功能的一个进步点,

    • 这不算数据类型

    • subscribe kuangshenshuo
      publish kuangshenshuo hello,world
      
  • Redis Stream 提供了消息的持久化和主备复制功能 Stream(5.0 版新增)

帮助

命令不区分大小写,而key是区分大小写的

  • set k1 v1
  • set K1 v2 这是 大,K1

help @类型名词

help @Stream

  XACK key group ID [ID ...]
  XADD key ID field string [field string ...]

String

mset和get incrby decr strlen setnx

最常用

  • set key value
    get key

同时设置/获取多个键值

  • MSET key value [key value …
  • MGET key [key ……]
  • m 就是 more的意思

数值增减

  • 递增数字
    INCR key
  • 增加指定的整数
    INCRBY key increment
  • 递减数值
    DECR key
  • 减少指定的整数
    DECRBY key decrement

获取字符串长度

  • STRLEN key

分布式锁

  • setnx key value

  • set key value [EX seconds] [PX milliseconds][NX|XX]

    • EX: key在多少秒之后过期
    • Px: key在多少毫秒之后过期
    • NX:当key不存在的时候,才创建key,效果等同于setnx
    • XX: 当key存在的时候,覆盖key
    set lock pay ex 20 nx
    set lock pay2 ex 20 xx
    
    ttl lock
    
使用场景:赞踩 递增编号 访问次数封IP

商品编号、订单号采用INCR命令生成

是否喜欢的文章,喜欢就赞一下,不喜欢就踩一下。

  • incr items:001
    get items:001
    "1"
    
  • 阅读数:只要点击了rest地址,直接可以使用incr key命令增加一个数字1,完成记录数字。

    比如说 ,我想知道什么时候封锁一个IP地址Incrby命令

    • 这个IP地址,访问的次数过多了

hash

h(m)set和get htall hlen hdel

redis hash 对应:

  • Map<String,Map<Object,Object>>

  • 次设置一个字段值
    HSET key field value

    • hset person id 1
      hset person name z3
      
  • 一次获取一个字段值
    HGET key field

    • hget person id
      "1"
      
  • 一次设置多个字段值
    HMSET key field value [field value …]

    • hset person score 99 birth 20201010
      (integer) 2
      
  • 一次获取多个字段值
    HMGET key field [field …]

    • hmget person score birth
      1) "99"
      2) "20201010"
      
  • 获取所有字段值

    • hgetall key 相当于java的MapEntry

    • hgetall person
      1) "id"
      2) "1"
      3) "name"
      4) "z3"
      5) "score"
      6) "99"
      7) "birth"
      8) "20201010"
      
  • 获取某个key内的全部数量

    • hlen
    • hlen person
      (integer) 4
  • 删除一个key

    • hdel
    • hdel person name
      (integer) 1
    • hlen person 删除后,还剩3个。
      (integer) 3
应用场景:购物车 对象局部更新

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

 hset shopcar:uid1023 334488 1
(integer) 1
#用户ID 加购了334488 1个

hincrby shopcar:uid1023 334488 1 #又加购了一个
(integer) 2
hget shopcar:uid1023 334488
"2"

hlen shopcar:uid1023 #当前买了 几种商品
(integer) 1

hgetall shopcar:uid1023 #全部选择
1) "334488"
2) "2"

更新对象,只更新对象的 一个值,不用全部更新。

  • **存储用户信息【id,name,age】**Hset(key,field,value)Hset(userKey,id,101)Hset(userKey,name,admin)Hset(userKey,age,23)----修改案例----Hget(userKey,id)Hset(userKey,id,102)为什么不使用String 类型来存储Set(userKey,用信息的字符串)Get(userKey)不建议使用String 类型

list

l和r push和pop lrange llen

向列表左边添加元素

  • LPUSH key value [value…]

  • lpush list1 1 2 3 #2插入1的左边,3插入2的左边。
    
    lrange list1 0 -1 #从左边第一个开始遍历,最左边的为 3。最右边的为1
    1) "3"
    2) "2"
    3) "1"
    

向列表右边添加元素

  • RPUSH key value [value …]

  • rpush list1 0 -1  #0插入,1的右边。 -1 插入0 的右边。最右边的为0
    
lrange list1 0 -1
1) "two"
2) "one"
3) "hou"

lpop list1 #弹出左边第一个
"two"

rpop list1 #弹出右边第一个
"hou"

查看列表

  • LRANGE key start stop

  • lrange list1 0 -1
    1) "3"
    2) "2"
    3) "1"
    4) "-1"
    5) "0"
    

获取列表中元素的个数

  • LLEN key
llen list1
(integer) 5
应用场景:秒杀 我关注的公众号

微信文章订阅公众号

  • 用户 关注的所有 公众号,放入一个 list 中。

大V作者李永乐老师和CSDN发布了文章分别是11和22

2阳哥关注了他们两个,只要他们发布了新文章,就会安装进我的List

lpush likearticle:阳哥id 11 22

3查看阳哥自己的号订阅的全部文章,类似分页,下面O~10就是一次显示10条

Irange likearticle:阳哥id 0 10

实现最新消息的排行,还可以利用List的push命令,将任务存在list集合中,同时使用另一个命令,将任务从集合中取出[pop]。Redis—list数据类型来模拟消息队列。【电商中的秒杀就可以采用这种方式来完成一个秒杀活动

set

sadd srem smembers scard spop srandmember

添加元素

  • SADD key member [member …]

  • sadd set1 1 2 3
    (integer) 3
    
    smembers set1
    1) "1"
    2) "2"
    3) "3"
    

删除元素

  • SREM key member [member …]

  • srem set1 1
    (integer) 1
    
    smembers set1
    1) "2"
    2) "3"
    
    del set1 #整个删除
    (integer) 1
    

获取集合中的所有元素

  • SMEMBERS key

判断元素是否在集合中

  • SISMEMBER key member

  • sismember set1 2
    (integer) 1
    

获取集合中的元素个数

  • SCARD key

  • scard set1
    (integer) 2
    

从集合中随机弹出一个元素,元素不删除

  • SRANDMEMBER key [数字]

  • srandmember set1
    "3"
    
    smembers set1
    1) "2"
    2) "3"
    
    srandmember set1 2
    1) "1"
    2) "2"
    

从集合中随机弹出一个元素,出一个删一个

  • SPOP key[数字]

  • spop set1
    "2"
    
集合运算 sdiff sinter sunion

集合的差集运算A-B (减去)

  • 属于A但不属于B的元素构成的集合

  • SDIFF key [key …]

  • sadd set1 1 2 3 4 5 6
    (integer) 6
    sadd set2 5 6 7 8 9 10
    (integer) 6
    
    sdiff set1 set2 #A中的值,不在B中
    1) "1"
    2) "2"
    3) "3"
    4) "4"
    

集合的交集运算AnB

  • 属于A同时也属于B的共同拥有的元素构成的集合

  • SINTER key [key …]

  • sinter set1 set2
    1) "5"
    2) "6"
    

集合的并集运算A U B

  • 属于A或者属于B的元素合并后的集合

  • SUNION key [key …]

  • sunion set1 set2
     1) "1"
     2) "2"
     3) "3"
     4) "4"
     5) "5"
     6) "6"
     7) "7"
     8) "8"
     9) "9"
    10) "10"
    
应用场景:抽奖 点赞过的用户 共同爱好 可能认识的人

微信抽奖小程序

srandmember set1 1 #特等奖 抽一个。抽奖 谁会这样写?没有权重
1) "5"

srandmember set1 2 #二等奖,设置3个

spop set 1 #使用这个,特等奖的人,领过后,就不能抽了


1用户ID,立即参与按钮
sadd key用户ID
sadd chouj:1010 1 2 3 #国庆抽奖,1 2 3 用户参与了

2显示已经有多少人参与了,上图23208人参加
SCARD key
scard chouj:1010
(integer) 3


SRANDMEMBER key 2
随机抽奖2个人,元素不删除
SPOP key 3
随机抽奖3个人,元素会删除

微信朋友圈点赞

1新增点赞
sadd pub:msgID 点赞用户ID1 点赞用户ID2

2取消点赞
srem pub:msgID 点赞用户ID

3展现所有点赞过的用户
SMEMBERS pub:msgID

4点赞用户数统计,就是常见的点赞红色数字
scard pub:msgID

5判断某个朋友是否对楼主点赞过
SISMEMBER pub:msgID 用户ID

微博好友关注社交关系

特殊之处:可以自动排重。比如说微博中将每个人的好友存在集合(Set)中,这样求两个人的共通好友的操作。我们只需要求交集即可。

共同关注:我去到局座张召忠的微博,马上获得我和局座共同关注的人

sadd s1 1 2 3 4
SINTER s1 s2

我关注的人也关注他(大家爱好相同)
我关注了华为余承东,余承东也关注了局座召忠,我和余总有共同的爱好
sadd s1 1 2 3 4 5
sadd s2 3 4 5 6 7

SISMEMBER s1 3 #看自己关注的有没有3号用户
SISMEMBER s2 3 #看余承东关注的有没有3号用户
#我关注的 余承东,也关注了 局座

QQ内推可能认识的人

sadd s1 1 2 3 4 5
sadd s2 3 4 5 6 7

SINTER s1 s2 #共同认识的同学。

SDIFF s1 s2 #这是 s2 可能认识的人。
SDIFF s2 s1 #这是 s1 可能认识的人。

zset

zadd zscore zrem zincrby zcard zcount
z(rev)range zrangebyscore

添加元素

  • ZADD key score member [score member …]

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

  • zadd zset1 100 mov1 20 mov2 #电影1 100分,电影2 20分
    

按照元素分数从小到大的顺序

返回索引从start到stop之间的所有元素

  • ZRANGE key start stop [WITHSCORES]

  • zrange zset1 0 -1
    1) "mov2"
    2) "mov1"
    
    zrevrange zset1 0 1 withscores #倒叙,查2条,展示分数
    1) "mov1"
    2) "100"
    3) "mov3"
    4) "55"
    

获取元素的分数

  • ZSCORE key member

  • zscore zset1 mov1
    "100"
    

删除元素

  • ZREM key member [member …]

  • zrem zset1 mov2
    (integer) 1
    
    zrange zset1 0 -1
    1) "mov1"
    

获取指定分数范围的元素

  • ZRANGEBYSCORE key min max [WITHSCORE] [LIMIT offset count]

  • zrangebyscore zset1 20 30
    1) "mov2"
    

增加某个元素的分数

  • ZINCRBY key increment member

  • zincrby zset1 2 mov2
    "22"
    

获取集合中元素的数量

  • ZCARD key

  • zcard zset1
    (integer) 2
    

获得指定分数范围内的元素个数

  • ZCOUNT key min max

  • zcount zset1 10 40
    (integer) 1
    
应用场景:商品排序 热搜

以某一个条件为权重,进行排序。京东:商品详情的时候,都会有一个综合排名,还可以按照价格进行排名。

  • 抖音热搜

  • 点赞数排序,搜索栏排序,好评数排序,微博7日热搜。

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

  • 思路:定义商品销售排行榜(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的时候有什么问题?

4.redis缓存过期淘汰策略
5.redis的LRU算法简介

  1. Redis除了拿来做缓存,你还见过基于Redis的什么用法?
  2. Redis做分布式锁的时候有需要注意的问题?
  3. 如果是Redis是单点部署的,会带来什么问题?
    1. 不用单机版,一主二从,+ 哨兵
    2. 一般用 3主3从,cluster 集群
  4. 集群模式下,比如主从模式,有没有什么问题呢?
  5. 那你简单的介绍一下 Redlock吧?你简历上写redisson,你谈谈
  6. Redis分布式锁如何续期?看门狗知道吗?

基本概念

多个服务间+保证同一时刻内+同一用户只能有一个请求(防止关键业务出现数据冲突和并发错误)

  • redis----redlock===> redisson

    • lock/unlock
  • Redis 是 单线程的,适合排队。

  • string----setnx
    自己手写就需要setnx+lua脚本

  • RedisCluster–redisson 集群手写不好,用 redisson

1 JVM层面的加锁
分布式微服务架构,拆分后各个微服务之间为了避免冲突和数:据故障而加入的一种锁,分布式锁

两个不同的东西

1 mysql
2 zookeeper
3 redis

项目搭建

  • 搭建两个项目 boot-redis01 和 02
Pom
<version>2.3.4.RELEASE</version> //老师用2.3.3

        <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>

        <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>

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId> <!--老师用3.1.0-->
        </dependency>
        <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>
        </dependency>
        <dependency> 这里无用
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

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

Lettuce是一个可伸缩线程安全的Redis客户端。多个线程可以共享同一个RedisConnection。它利用优秀netty NIO框架来高效地管理多个连接。

  • boot 2.X 应该redis操作都是这个 连接池,更快。
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    #      jedis:
    #        pool:
    #          max-active: 8
    #          max-wait: -1ms
    #          max-idle: 500
    #          min-idle: 0
    lettuce:
      shutdown-timeout: 0ms
      pool:
        max-active: 8 #连接池最大连接数(使用负值表示没有限制)默认8
        max-wait: -1ms #连接池最大阻塞等待时间(使用负值表示没有限制)默认-1
        max-idle: 500 #连接池中的最大空闲连接默认8
        min-idle: 0 #连接池中的最小空闲连接默认0
    database: 0
Lettuce
英
/ˈletɪs/
n.
[园艺] 生菜;莴苣;(美)纸币
RedisConofig
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory factory) {

        RedisTemplate<String, Serializable> r = new RedisTemplate<>();
        //设置工厂
        r.setConnectionFactory(factory);
        //设置 k 序列化
        r.setKeySerializer(new StringRedisSerializer());

        //设置序列化的 Value
        r.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        return r;
    }


    //秦老师,给的配置
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate222(RedisConnectionFactory factory) {//Lettuce ConnectionConfiguration
        //官方的。我们把 泛型参数改为 <String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);

        //定义 序列化 接口。Json解析 任意的对象
        Jackson2JsonRedisSerializer js = new Jackson2JsonRedisSerializer(Object.class);

        //使用jackson dataBind 包。和 上面的 ObjectMapper 一样。
        ObjectMapper om = new ObjectMapper();
        //可见性: 属性 访问器 ,自动删除
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //默认 非最终
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        js.setObjectMapper(om);//设置 om

        //定义 String 序列化
        StringRedisSerializer ss = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(ss);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(ss);


        // value序列化方式采用jackson
        template.setValueSerializer(js);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(js);

        //之后 配置 设置
        template.afterPropertiesSet();

        return template;
    }
}
redis设置产品
set goods:001 100
OK

get goods:001
"100"
Controller
@RestController
public class GoodController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}") //#读取更好吧,可以点
    private String serverPort;

    @RequestMapping("/buy_goods")
    public String buy_goods() {
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        //如果为null,就为0,否则 转成 int
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);

        if (goodsNumber > 0) {
            //售卖了一个,还是这么多
            int realNumber = goodsNumber - 1;

            stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));

            String r = "成功买到商品,还剩:" + realNumber + "\t 服务端口为:" + serverPort;
            System.out.println(r);
            return r;
        } else {
            System.out.println("意外情况,已经售空等" + serverPort);
        }
        return "fail";
    }
}

找问题

1. 单机版加锁 ReentrantLock
  • ReentrantLock 和 synchronized

1.单机版没加锁

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

  • 加锁后,单机版 多线程,可以了

    public String buy_goods() {
        synchronized (this) {
            //代码
        }
    }

1不见不散

  • 加:synchronized,造成 线程挤压和拥堵

2 过时不候

  • 我等着觉得时间太长了,我向放弃等待。
  • 给我一个规定的时间内,拿不到锁我再放弃
    private final Lock lock = new ReentrantLock();

    public String buy_goods() throws InterruptedException {

        if (lock.tryLock(5, TimeUnit.SECONDS)) {
            try {
                //执行代码
            } finally {
                lock.unlock();
            }
        } else {
            return "活动太火爆,请稍候重试";
        }
    }
分布式架构

nginx分布式散服务架构

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

  • 因为单机锁,是 java实现的 。部署2个相同的项目,就2个java了,没法互斥。

Nginx配置负载均衡

配置Nginx:upstream proxy_pass

/usr/local/nginx/conf
—nginx.conf

重启:
./nginx -s reload

启动
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf

关闭:./nginx -s stop

    upstream mynginx{
		server 192.168.31.1:1111 weight=1; #配置两个,自动负载均衡。访问到 本地80的,都代理到这个内网。
		server 192.168.31.1:2222 weight=1;
	}

	server {
        listen       80; #80端口的全部拦截
        server_name  localhost;

        location / {
            root   html;
        	proxy_pass http://mynginx; #这就是反向代理,内网的机器代理。代理到公网的应该是正向代理吧。
            index  index.html index.htm;
        }
}
    upstream kuang {
        server 127.0.0.1:8080 weight=2; #权重配置为2。3次请求,2次进入这里。
        server 127.0.0.1:8081 weight=1;
    }

    server { #4.多个server 配置不同的服务。
        listen       8081;
        server_name  localhost;
        
        location / {
            root   html;
            index  index.html index.htm;
            proxy_pass http://kuang; #指定负载均衡。
        }
       location /admin { #admin请求,配置到另一个台服务器
            xxx  192.168.1.47
        }
    }
分布式锁解决

上redis分布式锁setnx

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

  • setnx key value #如果key不存在,才设置

  • set key value [EX seconds] [PX milliseconds][NX|XX]

    • EX: key在多少秒之后过期
    • Px: key在多少毫秒之后过期
    • NX:当key不存在的时候,才创建key,效果等同于setnx
    • XX: 当key存在的时候,覆盖key
    set lock pay ex 20 nx
    set lock pay2 ex 20 xx
    
    ttl lock
    

https://redis.io/commands/set/

2. 分布式锁代码:redis.setIfAbsent
    @Autowired
    private StringRedisTemplate redis;

    public static final String REDIS_LOCK = "atguiguLock";

    @RequestMapping("/buy_goods")
    public String buy_goods() throws InterruptedException {
        
        String value = UUID.randomUUID() + Thread.currentThread().getName();
        //nx 如果不存在,才设置
        Boolean flag = redis.opsForValue().setIfAbsent(REDIS_LOCK, value);
        
        if (flag) {
            //抢锁成功,执行逻辑
        } else {
            //抢锁失败
            return "当前有人在购买";
        }

            String r = "成功买到商品,还剩:" + realNumber + "\t 服务端口为:" + serverPort;
            System.out.println(r);
        
        	//正常执行完毕,解锁。异常的话,也要解锁。
            redis.delete(REDIS_LOCK);
    }

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

		try{
			XXX
		} finally {
            redis.delete(REDIS_LOCK);
        }
出现宕机:加过期时间
  • 成功买到商品后,要删除 解锁,程序宕机,锁删除不掉。
  • 程序 重新启动后,无法购买。

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

        //nx 如果不存在,才设置
        Boolean flag = redis.opsForValue().setIfAbsent(REDIS_LOCK, value);
        if (flag) {
            redis.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);
            //抢锁成功,执行逻辑
        }
3. 解决原子性:setIfAbsent+ttl

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

  • 没有原子性,比如:刚设置好.setIfAbsent,服务器就挂了
        //nx 如果不存在,才设置
        Boolean flag = redis.opsForValue().setIfAbsent(REDIS_LOCK, value);
        redis.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);
        //抢锁成功,执行逻辑
  • 改为
        Boolean flag = redis.opsForValue().setIfAbsent(REDIS_LOCK, value,10,TimeUnit.SECONDS);
程序未执行完,锁消失了,误删他人
  • 线程A,比如:调用某个项目,执行慢,执行了 11秒,才执行完毕。

    • 在 10秒的时候,锁已经没了。
  • 线程B过来,设置了 锁。

    • 线程A执行结束,删除了锁,删除的是:线程B的锁。
    • B 执行完毕,我的锁呢?

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

4. 清理锁的问题 只删除自己的
只删除自己的

只能自己删除自己的,不许动别人的


			//判断加锁与解锁是不是同一个客户端
            if (redis.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)) {
                
                //若在此时,这把锁突然不是这个客户端的,则会误解锁
                redis.delete(REDIS_LOCK);
            }
解决原子性2种方案

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

  • 使用 lua 脚本。实际中 用这个,但是 面试官不让用。
  • 使用 redis 事务
mysql事务复习
开始一个事务start transaction, 或 set autocommit=off;

1.start transaction --开始一个事务
2.savepoint 保存点名--设置保存点
3.rollback to保存点名--回退事务
4.rollback --回退全部事务
5.commit--提交事务,所有的操作生效,不能回退


SELECT @@tx_isolation;
-- +-----------------+
-- | @@tx_isolation  |
-- +-----------------+
-- | REPEATABLE-READ |
-- +-----------------+

-- 3.把其中一个控制台的隔离级别设置 Read uncommitted
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
Redis事务

事务介绍
Redis的事务是通过MULTI,EXEC,DISCARD和WATCH这四个命令来完成。。

Redis的单个命令都是原子性的,所以这里确保事务性的对象是命令集合。

Redis将命令集合序列化并确保处于一事务的命令集合连续且不被打断的执行。

Redis不支持回滚的操作。

MULTI

注:用于标记事务块的开始。
Redis会将后续的命令逐个放入队列中,然后使用EXEC命令原子化地执行这个命令序列。语法:MULTl

EXEC
在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态。语法: EXEC

DISCARD
清楚所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
语法: DISCARD

WATCH
当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的状态。语法: WATCH key [key …]

注:该命令可以实现redis的乐观锁

UNWATCH
清除所有先前为一个事务监控的键。
语法:UNWATCH

  • 事务测试
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
  • 正常 watch 测试
127.0.0.1:6379> watch k1
OK

127.0.0.1:6379> multi
OK

127.0.0.1:6379> set k1 100
QUEUED
127.0.0.1:6379> set k2 222
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK

127.0.0.1:6379> unwatch
OK
  • 异常 watch 测试
127.0.0.1:6379> watch k1
OK

127.0.0.1:6379> multi
OK

127.0.0.1:6379> set k1 111  #其他线程:set k1 11。当前线程再监控着呢,发现 k1 被修改了。
QUEUED

127.0.0.1:6379> set k2 222
QUEUED

127.0.0.1:6379> exec #事务提交失败
(nil)

需要重新 watch
Redis调用lua脚本
if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
  • 案例使用自己的测试了,lua脚本 没报错,返回的为0,删除失败。

https://blog.csdn.net/qq_35377525/article/details/115229369

  • DefaultRedisScript
    
@Service
public class LuaScript{
    @Autowired
    private RedisTemplate redisTemplate;
 
    private DefaultRedisScript<Long> script;
 
    @PostConstruct
    public void init(){
        script = new DefaultRedisScript<Long>();
        //返回值为Long
        script.setResultType(Long.class);
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/tokenCheck.lua")));
    }
 
    public Long tokenCheck(String gamekey,String curtime){
 
        List<String> keys = new ArrayList();
        keys.add(gamekey);
        keys.add(curtime);
        
        //script:lua脚本
        //KEYS[1] KEYS[2],是要操作的键,可以指定多个,在lua脚本中通过KEYS[1], KEYS[2]获取
        //ARGV[1] ARGV[2],参数,在lua脚本中通过ARGV[1], ARGV[2]获取
        Long result = (Long) redisTemplate.execute(script,keys,0,0);
 
        return result;
    }
}

    //lua调redis
    Long token = luaScript.tokenCheck(RedisKeys.TOKENS+gameid,String.valueOf(new Date().getTime()));

img

5. 使用事务 解决删除非原子性
	finally {

            //实际工作中 依然用 lua脚本,
            while (true) {
                redis.watch(REDIS_LOCK);

                //判断一下 锁是不是自己的,才能删除
                if (redis.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)) {
                    //开启事务的功能
                    redis.setEnableTransactionSupport(true);

                    redis.multi();
                    redis.delete(REDIS_LOCK);
                    List<Object> list = redis.exec();

                    //如果为null,删除失败
                    if (list == null) {
                        //结束本次循环
                        continue;
                    } else {
                        //删除成功,结束循环
                        //break;
                    }
                }
                redis.unwatch();
                //break写在这里也行。删除失败了 走 continue,不会走到这里
                break;
            }//死循环
        }//finally
6. Jedis 使用脚本
Jedis工具类
public class RedisUtils {
    private static JedisPool jp;

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

        jp = new JedisPool(jc, "localhost", 6379);
    }

    public static Jedis getJedis() throws Exception {
        if (null != jp) {
            return jp.getResource();
        }
        throw new Exception("JedisPool is not ok");
    }
}
使用脚本实现
  • 关于解锁的:lua脚本
  • 亲测 可行。
if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
			//这个java14 才能用,直接三个引号存一段。""" """;
            String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                    "then\n" +
                    "    return redis.call(\"del\",KEYS[1])\n" +
                    "else\n" +
                    "    return 0\n" +
                    "end";
            Jedis jedis = RedisUtils.getJedis();

            try {
                //传递key
                List<String> lockList = Collections.singletonList(REDIS_LOCK);
                //传递 value,就是 参数的 ARGV,即:UUID.randomUUID() + Thread.currentThread().getName();
                List<String> valueList = Collections.singletonList(value);

                Object eval = jedis.eval(script, lockList, valueList);

                //如果 redis.call("del" 删除了一个键,返回1
                if ("1".equals(eval.toString())) {
                    System.out.println("删除成功");
                } else {
                    System.out.println("删除失败");
                }

            } finally {
                if (null != jedis) {
                    jedis.close();
                }
            }
7. 过期时间 大于 业务时间

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

  • 用完即删

  • 后台启动一个线程,扫描这个 key,快要到期了,给其续命。

集群+cAP对比zookeeper

  • Redis AP 分区容错+高可用

    • 从 master 获得锁后,还未 同步给 slave,master 宕机了。
    • redis异步复制造成的锁丢失,
      比如:主节点没来的及把刚刚set进来这条数据给从节点,就挂了。
  • Zookeeper CP

    • 保证 其他的 slave 结合 都和 主节点一样了,才 能获取到 锁。
  • 还存在两个问题:

    • 缓存续命
    • redis异步复制造成的锁丢失

Redis分布式锁如何续期?

redis集群环境下,我们自己写的也不oK

直接上RedLock之Redisson落地实现

Redisson

  • RedLock的落地实现

http://www.redis.cn/topics/distlock.html

RedisConfig
@Configuration
public class RedissonConfig {

    @Bean
    public Redisson redisson() {
        Config config = new Config();
		//使用单例的
        config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);

        return (Redisson) Redisson.create(config);
    }
}
8. Redisson实现
  • 测试依然是 可重入锁,可以上锁两次。解锁2次。
    public static final String REDIS_LOCK = "atguiguLock";
    @Autowired
    private Redisson redisson;

    @RequestMapping("/buy_goods")
    public String buy_goods() throws Exception {


        RLock lock = redisson.getLock(REDIS_LOCK);
        // lock.lock(30, TimeUnit.SECONDS);
        lock.lock();

        try {
            String result = redis.opsForValue().get("goods:001");
            //如果为null,就为0,否则 转成 int
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if (goodsNumber > 0) {
                //售卖了一个,还是这么多
                int realNumber = goodsNumber - 1;

                redis.opsForValue().set("goods:001", String.valueOf(realNumber));

                String r = "成功买到商品,还剩:" + realNumber + "\t 服务端口为:" + serverPort;
                System.out.println(r);

                return r;
            } else {
                System.out.println("意外情况,已经售空等" + serverPort);
            }
            return "fail";
        } finally {
            lock.unlock();
        }

    }
2023-02-03 15:35:20.198  INFO 5624 --- [  restartedMain] org.redisson.Version                     : Redisson 3.13.4
可能出现 线程不是同一个
  • 超高并发的时候
illegalMonitorStateException: attempt to unlock lock,not locked by current thread by node id:eda6385f
  • 尝试着 去解锁,但是:不能被当前的线程解锁。线程不是同一个。
修复
  • 就像释放连接,如果 不为null,才释放
            //如果是被锁定的。如果是 当前线程持有的话
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                //才进行解锁
                lock.unlock();
            }
另一种redis锁

业界也提供了多个现成好用的框架予以支持分布式锁,比如Redissonspring-integration-redis、redis自带的setnx命令,推荐直接使用。

另外,可基于redis命令和redis lua支持的原子特性,自行实现分布式锁。

总结

synchronized

  • 单机版oK,

上分布式

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

取消单机锁,

  • 上redijs分布式锁setnx

只加了锁,没有释放锁,

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

宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,

  • 需要有lockKey的过期时间设定

为redis的分布式锁key,增加过期时间

  • 此外,还必须要setnx+过期时间必须同一行

必须规定只能自己删除自己的锁,你不能把别人的锁删除了,

  • 防止张冠李戴,1删2,2删3

redis集群环境下,我们自己写的也不oK

  • 直接上RedLock之Redisson落地卖现

Redis内存

面试

4.redis缓存过期淘汰策略

粉丝反馈的面试题

  • Redis内存满了怎么办r

  • edis缓存淘汰策略

生产上你们你们的redis内存设置多少?

如何配置、修改redis的内存大小

如果内存满了你怎么办
redis清理内存的方式?

定期删除和惰性删除了解过吗

redis缓存淘汰策略

redis的LRu了解过吗?可否手写一个LRu算法

redis默认内存多少?在哪里查看?
如何设置修改?
真要打满了会怎么样?
如果Redis内存使用超出了设置的最大值会怎样?

查看Redis最大占用内存Zredis默认内存多少可以用?

一般生产上你如何配置?

如何修改redis内存设置

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

内存相关

默认最大内存配置文件
vim redis.config

:set nu #vim设置 行号

#找到这个 被注释的 配置
566 # maxmemory <bytes>

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

打开redis配置文件,设置maxmemory参数,maxmemory是bytes字节类型,注意转换。

  11 # 1k => 1000 bytes
  12 # 1kb => 1024 bytes
  13 # 1m => 1000000 bytes
  14 # 1mb => 1024*1024 bytes
  15 # 1g => 1000000000 bytes
  16 # 1gb => 1024*1024*1024 bytes
设置占用物理内存的3/4

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

  • redis底层也是 类似HashMap,负载因子 0.75
命令和配置 修改最大内存

如何修改redis内存设置

  • 通过修改文件配置

  • 通过命令修改团

maxmemory 104857600
#1024*1024*100
redis-serer /myrdis/redis.conf

进入连接后:
showndown 关闭连接
config get maxmemory
1) "maxmemory" #没有配置,无限大
2) "0"
    
config set maxmemory 104857600
    
info memory
# Memory
used_memory:895816
used_memory_human:874.82K
used_memory_rss:3702784
used_memory_rss_human:3.53M
used_memory_peak:2130280
used_memory_peak_human:2.03M ##人,实际存了多少数据
used_memory_peak_perc:42.05%
maxmemory:0
maxmemory_human:0B #最大内存,就是配置文件 可更改的
maxmemory_policy:noeviction
...

# 更改后看到
maxmemory:104857600
maxmemory_human:100.00M


info replication
# Replication
role:master
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
内存满了OOM

redis默认内存多少?在哪里查看?如何设置修改?真要打满了会怎么样?
如果Redis内存使用超出了设置的最大值会怎样?

  • 故意设置为1byte
127.0.0.1:6379>set k1 v1
(error)OOM command not allowed when used memory > ' maxmemory' .

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

redis过期键的删除策略

如果一个键是过期的,那它到了过期时间之后是不是马上就从内存中被被删除呢??
如果回答yes,你自己走还是面试官送你?
如果不是,那过期后到底什么时候被删除呢?﹖是个什么操作?

删除策略

三种不同的删除策略

定时删除

定时删除:set k1 v1 ex 10

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

缺陷:

Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。
立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,让CPU心累,时时需要删除,忙死。。。。。。o
这会产生大量的性能消耗,同时也会影响数据的读取操作。

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

数据到达过期时间,不做处理。等下次访问该数据时,如果未过期,返回数据;
发现已过期,删除,返回不存在。

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

定期删除

上面两种方案都走极端

  • 定期抽样key,判断是否过期
  • 漏网之鱼

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

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

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

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

举例:

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

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

还有漏洞吗?

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

必须要有一个更好的兜底方案…

内存淘汰策略登场

内存淘汰策略

LRU和LFU都是内存管理的页面置换算法。

LRU:最近最少使用(最长时间)淘汰算法(Least Recently Used)。LRU是淘汰最长时间没有被使用的页面。

LFU:最不经常使用(最少次)淘汰算法(Least Frequently Used)。LFU是淘汰一段时间内,使用次数最少的页面。

2 1 2 1 2 3 4

  • 若按LRU算法,应替换掉页面1。因为页面1是最长时间没有被使用的了,页面2和3都在它后面被使用过。
  • 若按LFU算法,应换页面3。因为在这段时间内,页面1被访问了2次,页面2被访问了3次,而页面3只被访问了1次,一段时间内被访问的次数最少。

有哪些(redis6.0.8版本)

# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select among five behaviors:
#
# volatile-lru -> Evict using approximated LRU among the keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.

# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.

# volatile-random -> Remove a random key among the ones with an expire set.
# allkeys-random -> Remove a random key, any key.

# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)

不在驱逐,只是返回一个错误
# noeviction -> Don't evict anything, just return an error on write operations.
#
# LRU means Least Recently Used
# LFU means Least Frequently Used

默认为:不在驱逐
# maxmemory-policy noeviction
eviction
英
/ɪˈvɪkʃn/
n.
逐出;赶出;收回

evict
英
/ɪˈvɪkt/
v.
驱逐,逐出

noeviction:不会驱逐任何key

allkeys-Iru:对所有key使用LRu算法进行删除

volatile-Iru:对所有设置了过期时间的key使用LRu算法进行删除

alkeys-random:对所有key随机删除

volatile-random:对所有设置了过期时间的key随机删除

volatile-ttl:删除马上要过期的key

allkeys-lfu:对所有key使用LFu算法进行删除

volatile-lfu:对所有设置了过期时间的key使用LFu算法进行删除

LRU是Least Recently Used的缩写,即最近最少使用

  • 时间上
Least
adj.
一点儿;最少的(little 的最高级);最微不足道的(little 的最高级形式);用于极小动植物的名称,如least shrew(little 的最高级形式)
adv.
最低程度地;最少,至少
n.
最不重要的事物,最微小的事物;至少,起码
pron.
最少量;起码
det.
最少的,最小的

LFU(least frequently used (LFU) page-replacement algorithm)。即最不经常使用页置换算法,

  • 频率上
frequently
英
/ˈfriːkwəntli/
adv.
经常地,频繁地

volatile
英
/ˈvɒlətaɪl/
adj.
易变的,动荡不定的,反复无常的;(情绪)易变的,易怒的,突然发作的;(液体或固体)易挥发的,易气化的;(计算机内存)易失的
n.
挥发物;<罕>有翅的动物

2个维度

  • 过期键中筛选

  • 所有键中筛选

四个方面

  • LRU
    LFU
    random
    ttl

noeviction:不会驱逐任何key
volatile-ttl:删除马上要过期的key

allkeys-Iru:对所有key使用LRu算法进行删除

allkeys-lfu:对所有key使用LFu算法进行删除

alkeys-random:对所有key随机删除

volatile-Iru:对所有设置了过期时间的key使用LRu算法进行删除

volatile-lfu:对所有设置了过期时间的key使用LFu算法进行删除

volatile-random:对所有设置了过期时间的key随机删除

经常使用和配置

即最近最少使用

allkeys-Iru:对所有key使用LRu算法进行删除

默认为:不在驱逐
# maxmemory-policy noeviction
maxmemory-policy allkeys-lru
config set maxmemory-policy allkeys-lru

config get maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-lru"

info memory
# Memory
used_memory:895816
maxmemory_human:100.00M
maxmemory_policy:allkeys-lru
policy
n.
政策,方针;(处事) 原则,策略;保险单

LRU

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,

选择最近最久未使用的数据予以淘汰。

  • 打开: 1 2 3 程序,满了,在打开4, 1从内存中 移除。

https://leetcode.cn/problems/Iru-cache/

题目

运用你所掌握的数据结构,设计和实现一个LRU(最近最少使用)缓存机制。它应该支持以下操作:获取数据get和写入数据put 。
获取数据get(key)-如果关键字(key)存在于缓存中,则获取关键字的值(总是正数),否则返回-1。
写入数据 put(key,value)-如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

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

capacity
英
/kəˈpæsəti/
n.
能力,才能;容积,容纳能力;职位,职责;功率,容积;生产量,生产能力
adj.
无虚席的,满场的
设计思想

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

2特性要求
2.1必须要有顺序之分,一区分最近使用的和很久没有使用的数据排序。2.2写和读操作一次搞定。
2.3如果容量(坑位)满了要删除最不长用的数据,每次新访问还要把新的数据插入到队头(按照业务你自己设定左右那一边是队头)

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

你是否可以在O(1)时间复杂度内完成这两种操作?
如果一次就可以找到,你觉得什么数据结构最合适??

LRU的算法核心是哈希链表

  • 本质就是HashMap+DoubleLinkedList
  • 时间复杂度是o(1),哈希表+邓
    仅向链表的结合体
继承 LinkedHashMap 重写remove Eldest Entry
  • 重写 remove Eldest Entry
Eldest
adj.
(尤指某一家庭中三个或三个以上的成员)年龄最大的,最老的
n.
(指三个或三个以上的人中)年龄最大的人;长子,长女(The eldest)

eld
n.
高龄;古人
public class LRUCache3<k, v> extends LinkedHashMap<k, v> {
    private int capacity;

    //accessOrder – the ordering mode -
    // true for access-order,
    // false for insertion-order
    public LRUCache3(int capacity) {
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

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

    public static void main(String[] args) {
        LRUCache3 l3 = new LRUCache3(3);

        l3.put(1, "a");
        l3.put(2, "b");
        l3.put(3, "c");
        l3.put(4, "d");
        //[2, 3, 4]
        System.out.println(l3.keySet());


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

}

/* access-order 访问顺序,参数为:true
        [2, 3, 4]
        [2, 4, 3]
        [2, 4, 3]
        [2, 4, 3]
        [4, 3, 5]*/

/*insertion-order 插入顺序,参数为:false。遇到相同的,顺序不变。
        [2, 3, 4]
        [2, 3, 4]
        [2, 3, 4]
        [2, 3, 4]
        [3, 4, 5]*/
无用 依赖 LinkedHashMap
public class LRUCache2 {
    //控制集合
    private LinkedHashMap<Integer, Integer> map = new LinkedHashMap();

    private int length;

    public LRUCache2(int capacity) {
        length = capacity;
    }

    public void put(int key, int value) {

        //先移除
        map.remove(key);

        //在添加
        map.put(key, value);

        if (map.size() > length) {
            //删除第一个元素,O1时间复杂度
            Map.Entry<Integer, Integer> next = map.entrySet().iterator().next();
            map.remove(next.getKey());
        }
    }

    public Integer get(int key) {
        Integer value = map.get(key);
        if (value != null) {
            //先移除
            map.remove(key);
            //在放入
            map.put(key, value);
        }
        return value;
    }

    public static void main(String[] args) {

        LRUCache2 c = new LRUCache2(2);
        c.put(1, 11);
        c.put(2, 22);
        System.out.println(c.get(1));
        c.put(3, 33);
        System.out.println(c.get(3));
        System.out.println(c.get(1));
        System.out.println(c.get(2));

    }
}
无用2 依赖 Map和List
public class LRUCache {
    //控制集合
    private HashMap<Integer, Integer> map = new HashMap();
    //控制 最先放入的
    private ArrayList<Integer> list = new ArrayList();

    //TreeMap

    private int length;

    public LRUCache(int capacity) {
        length = capacity;
    }

    public void put(Integer key, Integer value) {
        //list的大小 >= 长度
        if (list.size() >= length) {
            //获得最先放入的
            Integer num = list.get(0);
            //map 移除
            map.remove(num);
            //list 移除
            list.remove(0);
        }

        //key 可以替换
        map.put(key, value);

        //先移除,在添加
        list.remove(key);
        list.add(key);
    }

    public Integer get(Integer key) {
        Integer value = map.get(key);
        if (value != null) {
            //为Integer,按照值删除
            list.remove(key);
            //在添加
            list.add(key);
        }

        return value;
    }
}

手写双向链表

public class LRUCache4 {
    //map负责查找,构建一个虚拟的双向链表,它里面安装的就是一个个Node节点,作为数据载

    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) {
            this.key = key;
            this.value = value;

            this.prev = this.next = null;
        }
    }

    //2构建一个虚拟的双向链表,里面安防的就是我们的Node
    class DoubleLinkedList<K, V> {

        Node<K, V> head;
        Node<K, V> tail;

        public DoubleLinkedList() {
            head = new Node<>();
            tail = new Node<>();
            head.next = tail;
            tail.prev = head;
        }


        public void addHead(Node<K, V> node) {
            //插入到 头结点的后面
            //当前节点的 下一个为:头节点的下一个(现在是尾结点)
            node.next = head.next;
            //当前节点的 前一个,为 头节点。
            node.prev = head;

            //头节点的 下一个(现在是尾结点) 的 前一个 为 node
            head.next.prev = node;
            //头结点的 的下一个为 node
            head.next = node;
        }

        public void removeNode(Node<K, V> node) {
            //当前节点的 下一个 的前一个 = 当前节点的 前一个
            node.next.prev = node.prev;
            //当前节点的 前一个 的下一个 = 当前节点的下一个
            node.prev.next = node.next;

            //当前节点的指针 置空
            node.prev = null;
            node.next = null;
        }

        public Node getLast() {
            return tail.prev;
        }
    }

    private int cacheSize;
    Map<Integer, Node<Integer, Integer>> map;
    DoubleLinkedList<Integer, Integer> dll;

    public LRUCache4(int cacheSize) {
        this.cacheSize = cacheSize;
        map = new HashMap<>();
        dll = new DoubleLinkedList<>();
    }

    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        Node<Integer, Integer> node = map.get(key);
        dll.removeNode(node);
        dll.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);

            dll.removeNode(node);
            dll.addHead(node);
        } else {
            //满了
            if (map.size() == cacheSize) {
                Node last = dll.getLast();

                map.remove(last.key);
                dll.removeNode(last);
            }

            //新增逻辑
            Node<Integer, Integer> newNode = new Node<>(key, value);
            map.put(key, newNode);
            dll.addHead(newNode);
        }
    }

    public static void main(String[] args) {

        LRUCache4 l3 = new LRUCache4(3);

        l3.put(1, 1);
        l3.put(2, 2);
        l3.put(3, 3);
        l3.put(4, 4);
        //[2, 3, 4]
        System.out.println(l3.map.keySet());


        l3.put(3, 3);
        System.out.println(l3.map.keySet());
        l3.put(3, 3);
        System.out.println(l3.map.keySet());
        l3.put(3, 3);
        System.out.println(l3.map.keySet());
        l3.put(5, 5);
        System.out.println(l3.map.keySet());
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值