redis-学习-由入门到精通、常见的面试题目,redis分布式锁、一篇就够了

修订日期 内容
2021-3-27 初稿

安装运行

下载

官方下载地址:http://download.redis.io/releases/
github下载:https://github.com/redis/redis/tags
Windows版本:https://github.com/microsoftarchive/redis/tags

  • 官网网速你懂的,建议在github下载

安装(linux系统)

以本人下载的redis-6.0.0.tar.gz为例

tar -zxvf redis-6.0.0.tar.gz
cd redis-6.0.0/
make

make 后查看由没有报错信息,本人有如下报错:

MAKE hiredis
cd hiredis && make static 
make[3]: Entering directory `/usr/local/redis-6.0.0/deps/hiredis'
cc -std=c99 -pedantic -c -O3 -fPIC   -Wall -W -Wstrict-prototypes -Wwrite-strings -Wno-missing-field-initializers -g -ggdb net.c
make[3]: cc: Command not found
make[3]: *** [net.o] Error 127
make[3]: Leaving directory `/usr/local/redis-6.0.0/deps/hiredis'
make[2]: *** [hiredis] Error 2
make[2]: Leaving directory `/usr/local/redis-6.0.0/deps'
make[1]: [persist-settings] Error 2 (ignored)
    CC adlist.o
/bin/sh: cc: command not found
make[1]: *** [adlist.o] Error 127
make[1]: Leaving directory `/usr/local/redis-6.0.0/src'
make: *** [all] Error 2

这是因为缺少gcc编译工具导致,需要安装

yum install gcc

# 查看版本 (默认4.8.5)
gcc -v

#redis6需要4.9以上版本,所以需要升级gcc版本
yum install centos-release-scl scl-utils-build

#安装gcc8
yum install -y devtoolset-8-toolchain

# 生效 后再次查看
scl enable devtoolset-8 bash
gcc -v

再次进入安装redis 并测试

make
#make成功后进入src/目录 查询会有./redis-server的文件,运行
./redis-server &
# 进入客户端
./redis-cli
127.0.0.1:6379> set redis redis6.0.0
OK
127.0.0.1:6379> get redis
"redis6.0.0"

# 成功

基本数据结构(五种)

一.String

String 是最基本的数据类型,实际运用最为广泛,采用key-value形式。

基本操作

127.0.0.1:6379> set user admin
OK
127.0.0.1:6379> get user
"admin"

二.Hash

Hash是一个键对应多个属性,如:{user:{name:‘admin’,role:‘superadmin’}} 这样的数据,实际应用比较多。

基本操作

127.0.0.1:6379> hmset user username admin role superadmin
OK
127.0.0.1:6379> hmget user username
# 实际可以根据id存储用户相关的信息:如
127.0.0.1:6379> hmset user:1 username admin role superadmin
OK
127.0.0.1:6379> hmget user:1 username
1) "admin"
127.0.0.1:6379> hmget user:1 username role
1) "admin"
2) "superadmin"

三、List

简单的有序字符串列表,用来存储如:{‘user’:[‘admin’,‘manager’,‘system’]},类似这样的结构,可以不断的添加key中的value

基本操作

127.0.0.1:6379> lpush user admin superadmin
(integer) 2
127.0.0.1:6379> lpush user system
(integer) 3
127.0.0.1:6379> lrange user 0 10
1) "system"
2) "superadmin"
3) "admin"

四、Set

Set与List结构相似,通过hash表实现的无序集合,value不能重复。添加、删除、查找效率都比List高。

基本操作

127.0.0.1:6379> sadd user admin superadmin
(integer) 2
# 添加重复的value无效
127.0.0.1:6379> sadd user admin superadmin
(integer) 0
127.0.0.1:6379> sadd user system
(integer) 1
127.0.0.1:6379> SMEMBERS user
1) "admin"
2) "system"
3) "superadmin"

五、zset(sorted set)

Zset与set相比是有序且能快速访问的集合,zset中每一个value都带有一个分数score

基本操作

127.0.0.1:6379> del user
(integer) 1
127.0.0.1:6379> zadd user 1 admin
(integer) 1
127.0.0.1:6379> zadd user 1 admin
(integer) 0
127.0.0.1:6379> zadd user 2 superadmin
(integer) 1
127.0.0.1:6379> zadd user 3 system
(integer) 1
127.0.0.1:6379> ZRANGE user 1 2
1) "superadmin"
2) "system"

什么是缓存雪崩?如何解决

在了解缓存雪崩之前我们先了解下缓存的过期时间。

缓存失效(过期)

# 设置10秒过期
127.0.0.1:6379> set user admin ex 10
OK
# 查看剩余有效时间
127.0.0.1:6379> ttl user
(integer) 7
127.0.0.1:6379> get user
"admin"
127.0.0.1:6379> ttl user
(integer) -2
# 过期后无法获取
127.0.0.1:6379> get user
(nil)

缓存雪崩

一般在redis会结合数据库一起使用,在redis中查询不到数据时,或缓存服务宕机会查询数据库,并将查到的数据放入redis中。
试想一下,当大批量的缓存在同一时间失效,恰好此时大量的用户请求查询不到缓存,都进入数据库,导致数据库服务宕机,这种现象称之为缓存雪崩。

解决方案

  1. 错峰设置缓存过期时间
  2. redis服务问题可以设置集群,增加服务器

缓存穿透

当大量的请求请求一定不存在的数据时,则可能会有大量的请求频繁的查询数据库,这就是缓存穿透

解决方案

  1. 使用布隆过滤器,将所有数据放入布隆过滤器中(类似一个大的HashMap),如过查询在布隆过滤器中查询不到数据库时不需要查询数据库
  2. 粗暴的将一个空数据也放入缓存中,无论改数据是否真的不存在还是系统出现异常,尽可能设置一个较短的过期时间,该方式可能会产生两个问题:
    1. redis中可能会存在大量的空key
    2. 可能会出现缓存中判定为空的数据实际上数据库中已经存在

缓存击穿

缓存击穿是指一个非常热点的key出现过期时,大量的请求访问数据库时造成的宕机。

解决方案(2种)

  1. 设置key永不过期
  2. 使用分布式锁,在过期时只让一个线程去查数据库

内存淘汰策略

  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(常用)
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。

持久化

Redis分布式锁

原理:利用redis作为分布式锁,主要利用的redis指令
setnx key value :setnx的全称 set if not exists ,当key不存在时才插入成功,已存在则会插入失败

以实现扣减库存为例做如下设计

方案一

public String testLock(){
    
    
		// 1. 尝试加锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_KEY, "");
        // 2.加锁失败直接返回
        if(!success){
    
    
            return "加锁失败";
        }
        try{
    
    
			//3.加锁成功,处理业务逻辑
            String num = stringRedisTemplate.opsForValue().get(STOCK_KEY);
            logger.info("加锁成功,查询到剩余库存数量:{}",num);
            if(Integer.valueOf(num) > 0){
    
    
                stringRedisTemplate.opsForValue().decrement(STOCK_KEY);
                logger.info("扣减库存成功");
            } else {
    
    
                logger.info("产品已卖完了");
            }
        } catch (Exception e){
    
    
            logger.error("加锁业务逻辑处理异常",e);
        } finally {
    
    
        	//4.处理完业务逻辑后释放锁
            stringRedisTemplate.delete(LOCK_KEY);
        }


        return null;
    }

上面的方案一存在什么问题呢?
假设在执行try至catch的内容时出现宕机或关闭机器,则不会执行到stringRedisTemplate.delete(LOCK_KEY);
下次重启完服务后则redis中一直存在这个KEY,导致所有线程均不能加锁成功,即出现死锁的现象,如何解决呢?对key 设置一个过期时间行不行呢?看方案二

方案二

//方案二与方案一的基础上将
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_KEY, "");
//更改为 设置一个过期时间
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_KEY, "",10, TimeUnit.SECONDS);
        

那么方案二是否完美了呢?其实也会出现如下问题:

1.如果线程①处理业务实际超过设置的超时时间,线程①自动释放锁,线程②进入处理业务逻辑的过程中,线程①执行了第四步中删除key的代码,等于线程①释放了线程②的锁,则会导致线程③进入
2.如果处理业务实际超过设置的超时时间以后,redis中的key失效,也就会释放锁,那么其他线程加锁成功就会导致库存重复扣减的问题
如果解决呢?
解决问题一

方案三

以方案一为基础做如下修改:大致方案,仅删除自己加的锁,避免自己的锁过期,删除其他线程刚加的锁

// 步骤1.做如下改动
String uuid = UUID.randomUUID().toString();
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_KEY, uuid ,10, TimeUnit.SECONDS);

//步骤4修改如下
String lockValue = stringRedisTemplate.opsForValue().get(LOCK_KEY);
//仅删除自己加的锁,避免自己的锁过期,删除其他线程刚加的锁
if(uuid.equals(lockValue)){
    
    
    stringRedisTemplate.delete(LOCK_KEY);
}

完整代码

public String testLock(){
    
    
        String uuid = UUID.randomUUID().toString();
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_KEY, uuid ,10, TimeUnit.SECONDS);

        if(!success){
    
    
            logger.error("加锁失败");
            return "failed";
        }
        try{
    
    
            String num = stringRedisTemplate.opsForValue().get(STOCK_KEY);
            logger.info("加锁成功,查询到剩余库存数量:{}",num);
            if(Integer.valueOf(num) > 0){
    
    
                stringRedisTemplate.opsForValue().decrement(STOCK_KEY);
                logger.info("扣减库存成功");
            } else {
    
    
                logger.info("产品已卖完了");
            }

        } catch (Exception e){
    
    
            logger.error("加锁业务逻辑处理异常",e);
        } finally {
    
    
            String lockValue = stringRedisTemplate.opsForValue().get(LOCK_KEY);
            if(uuid.equals(lockValue)){
    
    
                stringRedisTemplate.delete(LOCK_KEY);
            }
        }


        return null;
    }

那方案二中的第二个问题还是没有解决,提供如下解决思路:
在设置过期时间比如10秒,则开启一个定时任务每3秒中执行一次看是否处理完成,如果未处理完则延长超时时间的3秒(1 /3可以跟进实际业务自己定义)。
上述的一些问题,其他第三方框架已经提供了完整的解决方案,如redission,比较推荐使用,避免重复造车轮。

第三方分布式锁框架redisson

一:引入pom文件

<dependency>
2    <groupId>org.redisson</groupId>
3    <artifactId>redisson</artifactId>
4    <version>3.11.1</version>
5 </dependency>
1.创建锁
RLock rlock = redisson.getLock(LOCK_KEY);
2.加锁 下面的方法为阻塞式方法,redisson提供了非常多的锁种类,可以跟进实际业务情况自己选择
rlock.lock(30,TimeUnit.SECONDS);//阻塞30秒

// 处理业务逻辑
3.释放锁
finally{
    
    
	rlock.unlock();
}

Guess you like

Origin blog.csdn.net/weixin_48470176/article/details/115269709