Redis总结


前言

回顾一下最近使用到的Redis知识点,并不齐全。Redis与传统的数据库不同的是它的数据是存放在内存中的,而且还是单线程的,所以读写速度非常快。Redis还支持丰富的数据类型以及支持数据的持久化。


一、常见的数据类型

1.string

常用命令:

set key value #设置 key-value 类型的值
get key # 根据 key 获得对应的 value

mset k1 v1 k2 v2 k3 v3 #同时设置多个值
mget k1 k2 k3 #同时获取多个值

incr num #类似于java的自增操作,num+1
incrby num increment #递增increment
decr num #递减num
decrby num decrement #递减decrement 

exists key  # 判断某个 key 是否存在

strlen key # 返回 key 所储存的字符串值的长度。

del key # 删除某个 key 对应的值

expire key  60 # 数据在 60s 后过期

setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)

ttl key # 查看数据还有多久过期

set key value [EX seconds] [PX milliseconds] [NX|XX]
#EX 在多少秒后过期
#PX 在多少毫秒后过期
#NX 当key不存在时才创建
#XX 当key存在时,覆盖key

使用场景:用在一些计数的场景,比如点赞、文章的点击量。

2.hash

常用命令

hmset key field v1 field2 v2

hexists key field # 查看 key 对应的 field 中指定的字段是否存在。

hget key field # 获取存储在哈希表中指定字段的值。

hgetall key # 获取在哈希表中指定 key 的所有字段和值

hkeys key # 获取 key 的所有field

hvals key # 获取key下面field的所有value

使用场景:我用在了购物车和上架秒杀活动时存放活动的数据。
在这里插入图片描述

3.list

常用命令

rpush myList value1 # 向 list 的右边添加元素

rpush myList value2 value3 # 向list的最右边添加多个元素

lpop myList # 将 list的最左边元素取出

lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end

lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒数第一

llen myList #查询长度 

4.set

常用命令

sadd mySet value1 value2 value3 # 添加元素进去,不允许有重复元素
srem value3 #删除元素

smembers mySet # 查看 set 中所有的元素

scard mySet # 查看 set 的长度

srandmember mySet [数字] #从set中随机弹出一个元素,元素不删除
spop mySet [数字] #从set中随机弹出一个元素,元素删除

sismember mySet value1 # 检查某个元素是否存在set 中,只能接收单个元素

sadd mySet2 value2 value3

sdiff mySet1 mySet2 #差集运算
sinter mySet1 mySet2 #交集运算
sunion mySet1 mySet2 #并集运算

使用场景:

  1. 抽奖在这里插入图片描述

  2. 微信朋友圈点赞在这里插入图片描述

  3. 微博共同关注
    在这里插入图片描述

5.zset

常用命令:

zadd myZset 3.0 value1 # 添加元素到 zset 中 3.0 为权重

zadd myZset 2.0 value2 1.0 value3 # 一次添加多个元素

zcard myZset # 查看 zset 中的元素数量

zscore myZset value1 # 查看某个 value 的权重

zrange  myZset 0 -1 # 顺序输出某个范围区间的元素,0 -1 表示输出所有元素

zrange  myZset 0 1 # 顺序输出某个范围区间的元素,0 为 start  1 为 stop

zrevrange  myZset 0 1 # 逆序输出某个范围区间的元素,0 为 start  1 为 stop

使用场景:

  1. 根据商品销量对商品进行排序显示
    在这里插入图片描述

二、分布式缓存

1.使用场景

首先我们有一个系统的首页需要展示大量不经常修改的数据,首页是一个系统访问量较大的页面,如果我们每次访问都需要去mysql数据库查询数据回显,那肯定会比较慢,而且还会给数据库添加压力,这时候我们就可以使用缓存,流程如下图。如果是单机系统,那我们可以使用Map把我们的数据存在本地即可,但如果是分布式系统,使用Map的话就会造成数据不一致。所以我们一般使用中间件来作为缓存,这时我们就可以使用我们的Redis了。
在这里插入图片描述

2.使用Redis作为缓存

以下是我自己项目中的例子:

public Map<String, List<Catelog2Vo>> getCatalogJson() {
        //1、加入缓存逻辑,缓存中存的数据是json字符串。
        //JSON跨语言,跨平台兼容。
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)) {
            //2、缓存中没有,查询数据库
            System.out.println("缓存不命中....调用方法查询数据库并存入缓存...");
            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
            return catalogJsonFromDb;
        }

        System.out.println("缓存命中....直接返回....");
        //转为我们指定的对象。
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        return result;
    }

以上就是最简单的使用逻辑,但是使用缓存还会有很多问题,我们先介绍一下使用缓存会出现的问题。

3.缓存问题

a、缓存穿透

介绍:

指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

解决:

null结果缓存,并加入短暂过期时间。

b、缓存雪崩

介绍:

缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

解决:

原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

c、缓存击穿

介绍:

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

解决:

加锁。大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db。


了解了上述三个问题,我们想要解决前两个问题比较简单,就是每次缓存时即使是空数据也缓存,但是过期时间要设置短一些,然后再加一个随机时间。难点是第三个加锁,这我们需要看下一个知识点。

三、分布式锁

1.本地锁

说到加锁,我们最先想到的就是synchronized,我们就先试试使用synchronized。

代码如下(省略了部分代码):

public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
        synchronized (this){
            String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
            if (!StringUtils.isEmpty(catalogJSON)) {
                //缓存不为null直接返回
                Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
                });
                return result;
            }
            System.out.println("查询数据库并返回数据");
            。。。。。。
            return result;
        }
    }

通过本地锁我们只可以锁住当前线程,如果想要锁住全部,那还是得用分布式锁。

2.使用Redis实现分布式锁

阶段一

在这里插入图片描述

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
        //1、占分布式锁。去redis占坑
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
        if (lock) {
            System.out.println("获取分布式锁成功...");
            Map<String, List<Catelog2Vo>> = getDataFromDb();
            redisTemplate.delete("lock");//删除锁
            return dataFromDb;
        } else {
            //加锁失败...重试
            return getCatalogJsonFromDbWithRedisLock();//自旋的方式
        }
    }

阶段二

在这里插入图片描述

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
        //1、占分布式锁。去redis占坑
        //2、设置过期时间,必须和加锁是同步的,原子的
        //3、给锁设置一个标记
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if (lock) {
            System.out.println("获取分布式锁成功...");
            Map<String, List<Catelog2Vo>> = getDataFromDb();
            String lockValue = redisTemplate.opsForValue().get("lock");
            if(uuid.equals(lockValue)){
                //删除我自己的锁
                redisTemplate.delete("lock");//删除锁
            }
            return dataFromDb;
        } else {
            //加锁失败...重试
            return getCatalogJsonFromDbWithRedisLock();//自旋的方式
        }
    }

阶段三

在这里插入图片描述

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
        //1、占分布式锁。去redis占坑
        //2、设置过期时间,必须和加锁是同步的,原子的
        //3、给锁设置一个标记
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if (lock) {
            System.out.println("获取分布式锁成功...");
            //加锁成功... 执行业务
            Map<String, List<Catelog2Vo>> dataFromDb;
            try {
                dataFromDb = getDataFromDb();
            } finally {
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                //4、删除锁,获取值对比+对比成功删除=>原子操作  lua脚本解锁
                Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
                        , Arrays.asList("lock"), uuid);
            }
            return dataFromDb;
        } else {
            //加锁失败...重试
            return getCatalogJsonFromDbWithRedisLock();//自旋的方式
        }
    }

3.使用Redisson实现分布式锁

redis官方并不推荐我们自己去实现分布式锁,而是向我们推荐使用Redlock,而Java对应的实现就是Redisson,官方网址:https://github.com/redisson/redisson

a、导入依赖

<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.0</version>
</dependency>

b、添加配置

@Configuration
public class MyRedissonConfig {

    /**
     * 所有对Redisson的使用都是通过RedissonClient对象
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson(@Value("${spring.redis.host}") String url) throws IOException {
        //1、创建配置
        //Redis url should start with redis:// or rediss://
        Config config = new Config();
        config.useSingleServer().setAddress("redis://"+url+":6379").setTimeout(1000).setPingConnectionInterval(1000).setPassword("密码");
        //2、根据Config创建出RedissonClient示例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

c、使用

@Controller
public class Lock {

    @Autowired
    RedissonClient redisson;

    @ResponseBody
    @GetMapping("lock")
    public String lock(){
        RLock lock = redisson.getLock("lock");
        lock.lock();
        try {
            System.out.println("加锁成功");
        }finally {
            lock.unlock();
        }
        return "hello";
    }
}

我们可以看到Redisson的使用与我们本地的ReentrantLock是一样的。然后它还会给我们的锁自动续期,如果业务运行时间超过锁的时间,它会自动给锁续上新的时间。然后业务运行完成后,锁会自动删除。其他锁的操作大家可以看官网:https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers

然后我们就可以使用Redisson来实现我们上面的业务代码了,就会变得非常简单。

public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
        RLock lock = redisson.getLock("CatalogJson-lock");
        lock.lock();
        Map<String, List<Catelog2Vo>> dataFromDb;
        try {
            dataFromDb = getDataFromDb();
        } finally {
            lock.unlock();
        }
        return dataFromDb;
    }

四、缓存一致性

上面,我们通过存储空值、加随机的过期时间和加锁解决了缓存穿透,缓存雪崩和缓存击穿等问题,那现在还有最后一个问题就是如何保证缓存的一致性。

1.双写模式

当我们写修改数据时,同时修改我们的缓存。但是可能会存在下图的问题:

在这里插入图片描述

2.失效模式

当我们修改数据时,同时删除对应的缓存。
这个方案也存在问题:当2号修改数据时,3号去读缓存,发现没有缓存,去读数据库,然后准备更新,此时2号修改完毕,删除缓存,但是3号已经读取到了旧数据,此时更新缓存,那还是会造成短期的数据不一致的问题。

在这里插入图片描述
解决:
1、缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新。
2、读写数据的时候,加上分布式的读写锁。
以上两种方案都还是存在短期的数据不一致问题,即多个线程同时更新会有问题。但是我们使用缓存存放数据本就不应该是实时性、一致性要求高的数据,所以只要保存最终一致性即可。

五、缓存过期淘汰策略

1.过期数据的删除策略

  1. 定时删除:只要过期立马删除。
  2. 惰性删除 :只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
  3. 定期删除 : 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。

但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。

怎么解决这个问题呢?答案就是:Redis 内存淘汰机制。

2.内存淘汰策略

  1. no-eviction:不会删除任何key
  2. volatile-lru:从已设置过期时间的key中挑选最近最少使用的数据淘汰
  3. volatile-ttl:从已设置过期时间的key中挑选将要过期的数据淘汰
  4. volatile-random:从已设置过期时间的key中任意选择数据淘汰
  5. allkeys-lru:当内存不足以容纳新写入数据时,从所有key中,移除最近最少使用的 key(这个是最常用的)
  6. allkeys-random:从所有key中任意选择数据淘汰
  7. volatile-lfu:从已设置过期时间的key中挑选最不经常使用的数据淘汰
  8. allkeys-lfu:当内存不足以容纳新写入数据时,从所有key中,移除最不经常使用的 key

2.读入数据

代码如下(示例):

data = pd.read_csv(
    'https://labfile.oss.aliyuncs.com/courses/1283/adult.data.csv')
print(data.head())

该处使用的url网络请求的数据。

六、持久化机制

1.RDB

Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

快照持久化是 Redis 默认采用的持久化方式,在 Redis.conf 配置文件中默认有此下配置:

save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 300 10          #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 60 10000        #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

2.AOF

与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:

appendonly yes

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到内存缓存 server.aof_buf 中,然后再根据 appendfsync 配置来决定何时将其同步到硬盘中的 AOF 文件。

在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:

appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec  #每秒钟同步一次,显式地将多个写命令同步到硬盘
appendfsync no        #让操作系统决定何时进行同步
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值