深入分析大厂面试题五

1 Redis高级部分

安装redis6.x

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

官网命令大全网址:http://www.redis.cn/commands.html

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

help @类型名词

  • 8大类型
    1. String(字符类型)
    2. Hash(散列类型)
    3. List(列表类型)
    4. Set(集合类型)
    5. SortedSet(有序集合类型,简称zset)
    6. Bitmap(位图)
    7. HyperLogLog(统计)
    8. GEO(地理)

1.1.1 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

分布式锁 
setnx key value
set key value [EX seconds] [PX milliseconds] [NX|XX]

应用场景
商品编号、订单号采用INCR命令生成
是否喜欢的文章
  • EX:key在多少秒之后过期
  • PX:key在多少毫秒之后过期
  • NX:当key不存在的时候,才创建key,效果等同于setnx
  • XX:当key存在的时候,覆盖key
set lock pay ex 10 NX

get lock

set lock order ex 10 NX

set lock pay ex 10 NX

get lock

1.1.2 hash

Map<String,Map<Object,object>>

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

一次获取一个字段值
HGET key field

一次设置多个字段值
HMSET key field value [field value ...]

一次获取多个字段值
HMGETkey field [field ....]

获取所有字段值
hgetall key

获取某个key内的全部数量
hlen

删除一个key
hdel

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

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

1.1.3 list

向列表左边添加元素
LPUSH key value [value ...]

向列表右边添加元素
RPUSH key value [value ....]

查看列表
LRANGE key start stop

获取列表中元素的个数
LLEN key

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

大V作者李永乐老师在CSDN发布了文章分别是11和22
阳哥关注了他们两2上,只要他们发布了新文章,就会安装进我的List
lpush likearticle:阳哥id 11 22
查看阳哥自己的号的订阅的全部文章,类似分布,下面0-10就是一次显示10条
lrange likearticle:阳哥id 0 10

1.1.4 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 ...]
集合的并集运算AUB 属于A或者属于B的元素合并后的集合
SUNION key [key ...]

应用场景

微信抽奖小程序

1 用户ID,立即参与按钮 sadd key 用户ID
2 显示已经有多少人参与了,上图23208人参加 SCARD key
3 抽奖(从set中任意选取N个中奖人) SRANDMEMBER key 2 随机抽奖2个人,元素不删除
SPOP key3 随机抽奖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

微博好友关注社交关系

image-20210122153304857.png

共同关注的人

sadd s1 1 2 3 4 5
sadd s2 3 4 5 6 7
取交集
SINTER s1 s2
共同关注:我去到以局座张召忠的微博,马上获得我和局座共同关注的人

我关注的人也关注他(大家爱好相同)

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

SISMEMBER s1 3
SISMEMBER s2 3

QQ内推可能认识的人

sadd s1 1 2 3 4 5
sadd s2 3 4 5 6 7
SINTER s1 s2
1) "3"
2) "4"
3) "5"

SDIFF s1 s2
1) "1"
2) "2"
SDIFF s2 s1
1) "6"
2) "7"

1.1.5 zset

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

添加元素
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

抖音热搜

1点击视频 ZINCRBY hotvcr:20200919 1八佰
ZINCRBY hotvcr:20200919 15 八佰 2 花木兰
2 展示当日排行前10条 ZREVRANGE hotvcr:20200919 0 9 withscores

1.1.6 Bitmap

BitMap 原本的含义是用一个比特位来映射某个元素的状态。由于一个比特位只能表示 0 和 1 两种状态,所以 BitMap 能映射的状态有限,但是使用比特位的优势是能大量的节省内存空间。

在 Redis 中,可以把 Bitmaps 想象成一个以比特位为单位的数组,数组的每个单元只能存储0和1,数组的下标在 Bitmaps 中叫做偏移量。

Redis 其实只支持 5 种数据类型,并没有 BitMap 这种类型,BitMap 底层是基于 Redis 的字符串类型实现的。

# 设置值,其中value只能是 0 和 1
setbit key offset value

# 获取值
getbit key offset

# 获取指定范围内值为 1 的个数
# start 和 end 以字节为单位
bitcount key start end

# BitMap间的运算
# operations 位移操作符,枚举值
  AND 与运算 &
  OR 或运算 |
  XOR 异或 ^
  NOT 取反 ~
# result 计算的结果,会存储在该key中
# key1 … keyn 参与运算的key,可以有多个,空格分割,not运算只能一个key
# 当 BITOP 处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作 0。返回值是保存到 destkey 的字符串的长度(以字节byte为单位),和输入 key 中最长的字符串长度相等。
bitop [operations] [result] [key1] [keyn…]

# 返回指定key中第一次出现指定value(0/1)的位置
bitpos [key] [value]

应用场景
1. 用户签到

很多网站都提供了签到功能,并且需要展示最近一个月的签到情况,这种情况可以使用 BitMap 来实现。
根据日期 offset = (今天是一年中的第几天) % (今年的天数),key = 年份:用户id。

如果需要将用户的详细签到信息入库的话,可以考虑使用一个异步线程来完成。

2. 统计活跃用户(用户登陆情况)

使用日期作为 key,然后用户 id 为 offset,如果当日活跃过就设置为1。具体怎么样才算活跃这个标准大家可以自己指定。

假如 20201009 活跃用户情况是: [1,0,1,1,0]
20201010 活跃用户情况是 :[ 1,1,0,1,0 ]

统计连续两天活跃的用户总数:
bitop and dest1 20201009 20201010 
# dest1 中值为1的offset,就是连续两天活跃用户的ID
bitcount dest1
统计20201009 ~ 20201010 活跃过的用户:
bitop or dest2 20201009 20201010 

3. 统计用户是否在线

如果需要提供一个查询当前用户是否在线的接口,也可以考虑使用 BitMap 。即节约空间效率又高,只需要一个 key,然后用户 id 为 offset,如果在线就设置为 1,不在线就设置为 0。

4. 实现布隆过滤器
大数据签到、重复点赞、订单重复评论等
# 假设某用户id=10,有篇文章叫artcleA,则设置artcleA上的位10成1
127.0.0.1:6379>  setbit artcleA 10 1
(integer) 0
# 统计文章artcleA的点赞人数
127.0.0.1:6379> bitcount artcle
(integer) 1
# 模拟重复点赞同一篇文章,返回了1,就可以根据返回值避免重复点赞
127.0.0.1:6379>  setbit artcleA 10 1
(integer) 1

1.1.7 HyperLogLog

  • Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
  • 在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
  • 但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素
pfadd 添加
影响基数估值则返回1否则返回0.若key不存在则创建
时间复杂度O(1)
pfadd m1 1 2 3 4 1 2 3 2 2 2 2
(integer) 1

pfcount 获得基数值
得到基数值,白话就叫做去重值(1,1,2,2,3)的插入pfcount得到的是3
可一次统计多个key
时间复杂度为O(N),N为key的个数
返回值是一个带有 0.81% 标准错误(standard error)的近似值.
pfadd m1 1 2 3 4 1 2 3 2 2 2 2
(integer) 1
pfcount m1
(integer) 4

pfmerge 合并多个key
取多个key的并集
命令只会返回 OK.
时间复杂度为O(N)
pfadd m1 1 2 3 4 1 2 3 2 2 2 2
(integer) 1
pfcount m1
(integer) 4
pfadd m2 3 3 3 4 4 4 5 5 5 6 6 6 1
(integer) 1
pfcount m2
(integer) 5
pfmerge mergeDes m1 m2
OK
pfcount mergeDes
(integer) 6

应用场景

说明:

  • 基数不大,数据量不大就用不上,会有点大材小用浪费空间
  • 有局限性,就是只能统计基数数量,而没办法去知道具体的内容是什么
  • 和bitmap相比,属于两种特定统计情况,简单来说,HyperLogLog 去重比 bitmap 方便很多
  • 一般可以bitmap和hyperloglog配合使用,bitmap标识哪些用户活跃,hyperloglog计数

一般使用:

  • 统计注册 IP 数
  • 统计每日访问 IP 数
  • 统计页面实时 UV 数
  • 统计在线用户数
  • 统计用户每天搜索不同词条的个数

1.1.8 GEO

Redis3.2 版本提供了GEO(地理信息定位)功能,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能,对于需要实现这些功能的开发者来说是一大福音。GEO功能是Redis的另一位作者Matt Stancliff 借鉴NoSQL数据库 Ardb 实现的,Ardb的作者来自中国,它提供了优秀的GEO功能。

Redis GEO 相关的命令如下:

# 添加一个空间元素,longitude、latitude、member分别是该地理位置的经度、纬度、成员
# 这里的成员就是指代具体的业务数据,比如说用户的ID等
# 需要注意的是Redis的纬度有效范围不是[-90,90]而是[-85,85]
# 如果在添加一个空间元素时,这个元素中的menber已经存在key中,那么GEOADD命令会返回0,相当于更新了这个menber的位置信息
GEOADD key longitude latitude member [longitude latitude member]
# 用于添加城市的坐标信息
geoadd cities:locations 117.12 39.08 tianjin 114.29 38.02 shijiazhuang 118.01 39.38 tangshan 115.29 38.51 baoding

# 获取地理位置信息
geopos key member [member ...]
# 获取天津的坐标
geopos cities:locations tianjin

# 获取两个坐标之间的距离
# unit代表单位,有4个单位值
  - m (meter) 代表米
  - km (kilometer)代表千米
  - mi (miles)代表英里
  - ft (ft)代表尺
geodist key member1 member2 [unit]
# 获取天津和保定之间的距离
GEODIST cities:locations tianjin baoding km

# 获取指定位置范围内的地理信息位置集合,此命令可以用于实现附近的人的功能
# georadius和georadiusbymember两个命令的作用是一样的,都是以一个地理位置为中心算出指定半径内的其他地理信息位置,不同的是georadius命令的中心位置给出了具体的经纬度,georadiusbymember只需给出成员即可。其中radiusm|km|ft|mi是必需参数,指定了半径(带单位),这两个命令有很多可选参数,参数含义如下:
# - withcoord:返回结果中包含经纬度。 
# - withdist:返回结果中包含离中心节点位置的距离。
# - withhash:返回结果中包含geohash,有关geohash后面介绍。
# - COUNT count:指定返回结果的数量。
# - asc|desc:返回结果按照离中心节点的距离做升序或者降序。
# - store key:将返回结果的地理位置信息保存到指定键。
# - storedist key:将返回结果离中心节点的距离保存到指定键。
georadius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]

georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]

# 获取geo hash
# Redis使用geohash将二维经纬度转换为一维字符串,geohash有如下特点:
# - GEO的数据类型为zset,Redis将所有地理位置信息的geohash存放在zset中。
# - 字符串越长,表示的位置更精确,表3-8给出了字符串长度对应的精度,例如geohash长度为9时,精度在2米左右。长度和精度的对应关系,请参考:https://easyreadfs.nosdn.127.net/9F42_CKRFsfc8SUALbHKog==/8796093023252281390
# - 两个字符串越相似,它们之间的距离越近,Redis利用字符串前缀匹配算法实现相关的命令。
# - geohash编码和经纬度是可以相互转换的。
# - Redis正是使用有序集合并结合geohash的特性实现了GEO的若干命令。
geohash key member [member ...]

# 删除操作,GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对地理位置信息的删除。
zrem key member

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

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

(boot+redis)

使用场景: 多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)

@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;
    }
}

上面代码优化

1.2.1 单机版没加锁

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

加synchronized 加ReentrantLock

@RestController
public class GoodController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

    private final Lock lock = new ReentrantLock();

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

        if (lock.tryLock()){
            try {
                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;
            }
        }finally {
                lock.unlock();
            }
        }else {
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);
        }
        return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
    }

}
@RestController
public class GoodController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

    @GetMapping("/buy_goods")
    public String buy_Goods(){
        synchronized (this) {
            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;
        }
    }
}

说明:在单机环境下,可以使用synchronized或Lock来实现。

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

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

1.2.2 nginx分布式微服务架构

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

Nginx配置负载均衡

启动Nginx并测试通过
/usr/local/nginx/conf

重启 ./nginx -s reload

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

高并发模拟 jmeter压测

image-20210122155952604.png

解决:

上redis分布式锁setnx

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

@RestController
public class GoodController {

    public static final String REDIS_LOCK_KEY = "lockhhf";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

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

        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        //setIfAbsent() 就是如果不存在就新建
        Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx

        if (!lockFlag) {  
            return "抢锁失败,┭┮﹏┭┮";
        }else {
            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);
                stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
            }else {
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
        }
    }
}

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

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

@RestController
public class GoodController {

    public static final String REDIS_LOCK_KEY = "lockhhf";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

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

        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        try{
            //setIfAbsent() 就是如果不存在就新建
            Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx

            if (!lockFlag) {
                return "抢锁失败,┭┮﹏┭┮";
            }else {
                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;
            }
        }finally {
            stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
        }
    }

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

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

@RestController
public class GoodController {

    public static final String REDIS_LOCK_KEY = "lockhhf";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

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

        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        try{
            //setIfAbsent() 就是如果不存在就新建
            Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx
            stringRedisTemplate.expire(REDIS_LOCK_KEY,10L, TimeUnit.SECONDS);
            if (!lockFlag) {
                return "抢锁失败,┭┮﹏┭┮";
            }else {
                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;
            }
       
        }finally {
            stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
        }
    }
}

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

@RestController
public class GoodController {

    public static final String REDIS_LOCK_KEY = "lockhhf";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

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

        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        try{
            //setIfAbsent() == setnx 就是如果不存在就新建,同时加上过期时间保证原子性
            Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value,10L, TimeUnit.SECONDS);

            if (!lockFlag) {
                return "抢锁失败,┭┮﹏┭┮";
            }else {
                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;
            }
        }finally {
            stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
        }
    }
}

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

image-20210122160926028.png

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

@RestController
public class GoodController {

    public static final String REDIS_LOCK_KEY = "lockhhf";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

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

             String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
            try{
                //setIfAbsent() == setnx 就是如果不存在就新建,同时加上过期时间保证原子性
                Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value,10L, TimeUnit.SECONDS);
                stringRedisTemplate.expire(REDIS_LOCK_KEY,10L, TimeUnit.SECONDS);
                if (!lockFlag) {
                    return "抢锁失败,┭┮﹏┭┮";
                }else {
                    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;
                }
               
                }finally {
                if (value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))){
                    stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
                }
                }
    }
}

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

解决:用redis自身的事务

  • 事务介绍

    • Redis的事务是通过MULTI,EXEC,DISCARD和WATCH这四个命令来完成。
    • Redis的单个命令都是原子性的,所有这里确保事务性的对象是命令集合
    • Redis将命令集合序列化并确保处于一事务的命令集合连接且不被打断的执行。
    • Redis不支持回滚的操作。
  • 相关命令

  • MULTI 注:用于标记事务块的开始

    • Redis会将后续的命令逐个放入队列中,然后使用EXEC命令原子化的执行这个命令序列。
  • EXEC

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

    • 清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
  • WATCH

    • 当某个事务需要按条件执行时,就要使用这个命令将给定的键状态设置为受监控的状态
  • UNWATCH

    • 清除所有先前为一个事务设置监控的键。
@RestController
public class GoodController {

    public static final String REDIS_LOCK_KEY = "lockhhf";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

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

        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

        try{
            //setIfAbsent() == setnx 就是如果不存在就新建,同时加上过期时间保证原子性
            Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value,10L, TimeUnit.SECONDS);
            stringRedisTemplate.expire(REDIS_LOCK_KEY,10L, TimeUnit.SECONDS);
            if (!lockFlag) {
                return "抢锁失败,┭┮﹏┭┮";
            }else {
                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;
            }
        }finally {
            while (true)
            {
                stringRedisTemplate.watch(REDIS_LOCK_KEY); //加事务,乐观锁
                if (value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))){
                    stringRedisTemplate.setEnableTransactionSupport(true);
                    stringRedisTemplate.multi();//开始事务
                    stringRedisTemplate.delete(REDIS_LOCK_KEY);
                    List<Object> list = stringRedisTemplate.exec();
                    if (list == null) {  //如果等于null,就是没有删掉,删除失败,再回去while循环那再重新执行删除
                        continue;
                    }
                }
                //如果删除成功,释放监控器,并且breank跳出当前循环
                stringRedisTemplate.unwatch();
                break;
            }
        }
       
    }
}

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

@RestController
public class GoodController {

    public static final String REDIS_LOCK_KEY = "lockhhf";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

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

        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

        try{
            //setIfAbsent() == setnx 就是如果不存在就新建,同时加上过期时间保证原子性
            Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value,10L, TimeUnit.SECONDS);
            stringRedisTemplate.expire(REDIS_LOCK_KEY,10L, TimeUnit.SECONDS);
            if (!lockFlag) {
                return "抢锁失败,┭┮﹏┭┮";
            }else {
                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;
            }
        }finally {
            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_KEY), 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();
                        }
                    }
        }

    }
}

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

Redis(AP)分布式锁如何续期?

集群+CAP对比zookeeper(CP)

redis异步复制造成的锁丢失, 比如:主节点没来的及把刚刚set进来这条数据给从节点,就挂了。
此时如果集群模式下,就得上Redisson来解决

redis集群环境下,我们自己写的也不OK, 直接上RedLock之Redisson落地实现
@RestController
public class GoodController {

    public static final String REDIS_LOCK_KEY = "lockhhf";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

    @Autowired
    private Redisson redisson;

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

        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

        RLock redissonLock = redisson.getLock(REDIS_LOCK_KEY);
        redissonLock.lock();
        try{
                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;

        }finally {
            redissonLock.unlock();
        }
    }
}

IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id:

出现这个错误的原因:是在并发多的时候就可能会遇到这种错误,可能会被重新抢占

不见得当前这个锁的状态还是在锁定,并且本线程持有

@RestController
public class GoodController {

    public static final String REDIS_LOCK_KEY = "lockhhf";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

    @Autowired
    private Redisson redisson;

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

        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

        RLock redissonLock = redisson.getLock(REDIS_LOCK_KEY);
        redissonLock.lock();
        try{
            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;

        }finally {
            //还在持有锁的状态,并且是当前线程持有的锁再解锁
            if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()){
                redissonLock.unlock();
            }

        }
    }
}

1.2.4 总结

synchronized      单机版oK,上分布式

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

取消单机锁         上redis分布式锁setnx

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

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

为redis的分布式锁key,增加过期时间
此外,还必须要setnx+过期时间必须同一行的原子性操作

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

lua或者事务

redis集群环境下,我们自己写的也不OK
直接上RedLock之Redisson落地实现

1.3 redis缓存过期淘汰策略

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

1.3.1 Redis内存满了怎么办

redis默认内存多少?在哪里查看? 如何设置修改?

查看Redis最大占用内存
打开redis配置文件,设置maxmemory参数,maxmemory是bytes字节类型,注意转换。

redis默认内存多少可以用?
如果不设置内存大小或者内存大小设置为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3gb

一般生产上你如何配置?
一般推荐Redis设置内存为最大物理内存的四分之三,也就是0.75

如何修改redis内存设置
通过修改文件配置 maxmemory
通过命令修改 config set maxmemory xx

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

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

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

config set maxmemory 1

config get maxmemory

set k1 v1
(error) OOM command not allowed when used memory > 'maxmemory'.

结论

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

1.3.2 redis缓存淘汰策略

往redis里写的数据是怎么没了的

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

三种不同的删除策略
定期删除
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时间过多地消耗在删除过期键上面。如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除束略一样,出现浪费内存的情况。因此,如果采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作的执行时长和执行频率。

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

1 定期删除时,从来没有被抽查到

2 惰性删除时,也从来没有被点中使用过

上述2步骤======> 大量过期的key堆积在内存中,导致redis内存空间紧张或者很快耗尽

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

内存淘汰策略

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

总结

2*4得8

2个维度
过期键中筛选
所有键中筛选

4个方面
LRU LFU random ttl

如何配置、修改

config set maxmemory-policy allkeys-lru
或者配置文件修改

1.4 redis的LRU算法简介

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

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

算法来源 https://leetcode-cn.com/problems/lru-cache/

image-20210122163449962.png

设计思想

1 所谓缓存,必须要有读+写两个操作,按照命中率的思路考虑,写操作+读操作时间复杂度都需要为O(1)
 
2 特性要求分析
    2.1 必须有顺序之分,以区分最近使用的和很久没用到的数据排序。
    2.2 写和读操作 一次搞定。
    2.3 如果容量(坑位)满了要删除最不长用的数据,每次新访问还要把新的数据插入到队头(按照业务你自己设定左右那一边是队头) 
 
         查找快,插入快,删除快,且还需要先后排序-------->什么样的数据结构满足这个问题?
 
你是否可以在O(1)时间复杂度内完成这两种操作?
 
如果一次就可以找到,你觉得什么数据结构最合适??

LRU的算法核心是哈希链表

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

编码手写如何实现LRU

参考LinkedHashMap,依赖JDK

import java.util.LinkedHashMap;
import java.util.Map;

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());
    }
}

/**
 * true
 * [1, 2, 3]
 * [2, 3, 4]
 * [2, 4, 3]
 * [2, 4, 3]
 * [2, 4, 3]
 * [4, 3, 5]
 * */

/**
 [1, 2, 3]
 [2, 3, 4]
 [2, 3, 4]
 [2, 3, 4]
 [2, 3, 4]
 [3, 4, 5]
 */

不依赖JDK

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCacheDemo{

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

    //1.构造一个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;
        }

        //3. 添加到头
        public void addHead(Node<K,V> node) {
            node.next = head.next;
            node.prev = head;
            head.next.prev = node;
            head.next = node;
        }

        //4.删除节点
        public void removeNode(Node<K, V> node) {
            node.next.prev = node.prev;
            node.prev.next = node.next;
            node.prev = null;
            node.next = null;
        }

        //5.获得最后一个节点
        public Node getLast() {
            return tail.prev;
        }
    }

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

    public LRUCacheDemo(int cacheSize) {
        this.cacheSize = cacheSize;//坑位
        map = new HashMap<>();//查找
        doubleLinkedList = new DoubleLinkedList<>();
    }

    public int get(int key){
        if (!map.containsKey(key)){
            return -1;
        }

        Node<Integer, Integer> node = map.get(key);
        doubleLinkedList.removeNode(node);
        doubleLinkedList.addHead(node);

        return node.value;
    }

    public void put(int key, int value) {
        if (map.containsKey(key)){  //update
            Node<Integer, Integer> node = map.get(key);
            node.value = value;
            map.put(key, node);

            doubleLinkedList.removeNode(node);
            doubleLinkedList.addHead(node);
        }else {
            //坑位满了
            if (map.size() == cacheSize) {
                Node<Integer,Integer> lastNode = doubleLinkedList.getLast();
                map.remove(lastNode.key);
                doubleLinkedList.removeNode(lastNode);
            }

            //新增一个
            Node<Integer, Integer> newNode = new Node<>(key, value);
            map.put(key,newNode);
            doubleLinkedList.addHead(newNode);

        }
    }

    public static void main(String[] args) {

        LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3);

        lruCacheDemo.put(1,1);
        lruCacheDemo.put(2,2);
        lruCacheDemo.put(3,3);
        System.out.println(lruCacheDemo.map.keySet());

        lruCacheDemo.put(4,1);
        System.out.println(lruCacheDemo.map.keySet());

        lruCacheDemo.put(3,1);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.put(3,1);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.put(3,1);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.put(5,1);
        System.out.println(lruCacheDemo.map.keySet());

    }
}

/**
 * true
 * [1, 2, 3]
 * [2, 3, 4]
 * [2, 4, 3]
 * [2, 4, 3]
 * [2, 4, 3]
 * [4, 3, 5]
 * */

/**
 [1, 2, 3]
 [2, 3, 4]
 [2, 3, 4]
 [2, 3, 4]
 [2, 3, 4]
 [3, 4, 5]
 */
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容