Redis基础

Redis基础

redis简介

redis是一个非关系型的数据库,完全基于内存,c语言编写,采用单线程。数据存储在内存中,读写速度非常快,因此被广泛用于缓存,适合读多写少的应用场景。

redis常见数据结构

String(字符串)

常用命令:set,get,decr,incr,mget等。
String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用:常规计数、定时器并发控制等。

Hash(哈希)

常用命令:hget,hset,hgetall 等。
hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以用 hash 数据结构来存储地址信息等

List(列表)

常用命令: lpush,rpush,lpop,rpop,lrange等
list 就是链表,Redis list的应用场景非常多,比如消息队列就可以用Redis的 list 结构来实现。list 的实现为一个双向链表,即可以支持反向查找和遍历,方便操作,不过带来了部分额外的内存开销。

Set(集合)

常用命令: sadd,spop,smembers,sunion 等
set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set是可以自动排重的。当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。

ZSet(有序集合)

常用命令: zadd,zrange,zrem,zcard等
zset 类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。举例:在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。

为什么使用缓存

缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力,所以企业会大量运用到缓存技术;但使用缓存也会带来一定的弊端,会牺牲一定的数据一致性和提高运维成本

缓存穿透

缓存穿透:多次去查询一个一定不存在的数据,因为这个数据不存在所以都会从数据库去查询,可能会导致数据库服务器压力过大,导致服务宕机,这就是缓存穿透。

解决办法:使用redisson实现的布隆过滤器。布隆过滤器就是在查缓存前加一层过滤器,主要用于检索一个元素是否在一个集合中,底层是先去初始化一个较大的数组,里面存放的二进制的0或1,在一开始都是0,需要把缓存里的所有key经过三次hash计算, 模于数组长度找到数据 的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一 个key的存在。查找的过程也是一样的 。可能会产生误判,但是可以通过设置,使误判率大概不会超过%5.

缓存击穿

缓存击穿:对于设置过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端数据库加载数据并回设到缓存,这个时候大并发的请求可能 会瞬间把数据库压垮,这就是缓存击穿。

解决办法:

  1. 使用互斥锁,当缓存失效时,不会立刻去数据库查询,而是使用redis的setnx去设置一个互斥锁,对应的线程再去竞争锁,得到锁的线程便可以去数据库查询,并将查询结果添加到缓存,而未竞争到锁的其他线程,便会一直重试获取缓存。
  2. 设置当前key逻辑过期,在设置key的时候,设置一个过期时间字段放入缓存中,不给当前key设置过期时间,当查询的时候,从redis取出数据后判断时候过期,如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据, 这个数据不是最新 。

选择数据的强一致性,建议使用分布式锁的方案,性能上可能没那么 高,锁需要等,也有可能产生死锁的问题。
选择key的逻辑删除,则优先考虑的高可用性,性能比较高,但是数据 同步这块做不到强一致。

缓存雪崩

缓存雪崩: 设置缓存时采用了相同的过期时间,导致缓存在某一时刻同 时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。

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

缓存更新策略

**内存淘汰:**redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据
**超时剔除:**当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存
**主动更新:**我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题

Redis做为缓存,数据的持久化

在Redis中提供了两种数据持久化的方式:RDB AOF

  1. RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当 redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。
  2. AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中, 当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复 数据

数据库和缓存不一致的解决方案

  1. 缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案
  2. 由系统本身完成,数据库与缓存的问题交由系统本身去处理
  3. 调用者只操作缓存,其他线程去异步处理数据库,实现最终一致

Redis的数据过期策略

  1. 惰性删除,在设置该key过期时间后,我们不去管它,当需要该key 时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。
  2. 定期删除,就是说每隔一段时间,我们就对一些key进行检查,删 除里面过期的key 定期清理的两种模式: SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配 置文件redis.conf 的 hz 选项来调整这个次数 FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms, 每次耗时不超过1ms

Redis的过期删除策略:惰性删除+定期删除两种策略进行配合使用。

Redis的数据淘汰策略

默认是noeviction,不删除任何数据,内存不足直接报错
可以在redis的配置文件中进行设置的,里面有两个非常重要的概念,一个 是LRU,另外一个是LFU LRU的意思就是最少最近使用,用当前时间减去最后一次访问时间,这个值 越大则淘汰优先级越高。LFU的意思是最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高 我们在项目设置的allkeys-lru,挑选最近最少使用的数据淘汰,把一些经常访问的key留在redis

Redis分布式锁如何实现

redis的单线程的,用了命令之后,只能有一个客户端对某一个key设置值,在没有过期或删除key的时候是其他客户端是不能设置这个key

Redis实现分布式锁有效时长

redissetnx指令不好控制这个问题,我们当时采用的 redis的一个框架redisson实现的。 在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁 住的一个业务还没有执行完成的时候,在redisson中引入了一个看门狗机 制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加 锁的持有时间,当业务执行完成之后需要使用释放锁就可以了 还有一个好处就是,在高并发下,一个业务有可能会执行很快,先客户1持 有锁的时候,客户2来了以后并不会马上拒绝,它会自旋不断尝试获取锁, 如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。

redisson实现的分布式锁

可以重入的。这样做是为了避免死锁的产生。这个重入其实 在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计 数,如果释放锁就会在计算上减一。在存储数据的时候采用的hash结构,大 key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value 是当前线程重入的次数

redisson实现的分布式锁能解决主从一致性的问题吗?

不能,比如,当线程1加锁成功后,master节点数据会异 步复制到slave节点,此时当前持有Redis锁的master节点宕机,slave节点被 提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的 master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问 题。 我们可以利用redisson提供的红锁来解决这个问题,它的主要作用是,不能 只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,并且要求在 大多数redis节点上都成功创建锁,红锁中要求是redis的节点数量要过半。这 样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的 master节点上的问题了。 但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的 很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接 使用红锁,并且官方也暂时废弃了这个红锁

redis工具类

public class RedisUtils{

    private final StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }
	//设置逻辑过期
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    //解决缓存穿透  缓存null值
    public <R,ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
        String key = keyPrefix + id;
        // 1.从redis查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(json)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(json, type);
        }
        // 判断命中的是否是空值
        if (json != null) {
            // 返回一个错误信息
            return null;
        }

        // 4.不存在,根据id查询数据库
        R r = dbFallback.apply(id);
        // 5.不存在,返回错误
        if (r == null) {
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 6.存在,写入redis
        this.set(key, r, time, unit);
        return r;
    }

    //使用逻辑过期解决缓存击穿
    public <R, ID> R queryWithLogicalExpire(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.从redis查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isBlank(json)) {
            // 3.存在,直接返回
            return null;
        }
        // 4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5.判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())) {
            // 5.1.未过期,直接返回信息
            return r;
        }
        // 5.2.已过期,需要缓存重建
        // 6.缓存重建
        // 6.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        // 6.2.判断是否获取锁成功
        if (isLock){
            // 6.3.成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R newR = dbFallback.apply(id);
                    // 重建缓存
                    this.setWithLogicalExpire(key, newR, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        // 6.4.返回过期信息
        return r;
    }

    //互斥锁解决缓存击穿
    public <R, ID> R queryWithMutex(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.从redis查询j缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(json)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(json, type);
        }
        // 判断命中的是否是空值
        if (json != null) {
            // 返回一个错误信息
            return null;
        }

        // 4.实现缓存重建
        // 4.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        R r = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2.判断是否获取成功
            if (!isLock) {
                // 4.3.获取锁失败,休眠并重试
                Thread.sleep(50);
                return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
            }
            // 4.4.获取锁成功,根据id查询数据库
            r = dbFallback.apply(id);
            // 5.不存在,返回错误
            if (r == null) {
                // 将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                // 返回错误信息
                return null;
            }
            // 6.存在,写入redis
            this.set(key, r, time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            // 7.释放锁
            unlock(lockKey);
        }
        // 8.返回
        return r;
    }

    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
}

消息队列

  • 消息队列:存储和管理消息,也被称为消息代理(Message Broker
  • 生产者:发送消息到消息队列
  • 消费者:从消息队列获取消息并处理消息

消息队列通常用来异步,削峰,解耦等操作

基于List实现消息队列

Redislist数据结构是一个双向链表,队列是入口和出口不在一边,因此我们可以利用:LPUSH 结合 RPOP、或者 RPUSH 结合 LPOP来实现。不过要注意的是,当队列中没有消息时RPOPLPOP操作会返回null,并不像JVM的阻塞队列那样会阻塞并等待消息。因此这里应该使用BRPOP或者BLPOP来实现阻塞效果

基于PubSub的消息队列

消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息

  • SUBSCRIBE channel [channel] :订阅一个或多个频道
  • PUBLISH channel msg :向一个频道发送消息
  • PSUBSCRIBE pattern[pattern]:订阅与pattern格式匹配的所有频道
基于Stream的消息队列

StreamRedis 5.0 引入的一种新数据类型,可以实现一个功能非常完善的消息队列。

GEO数据结构的基本用法

GEO就是Geolocation的简写形式,代表地理坐标。Redis在3.2版本中加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据经纬度来检索数据。常见的命令有:

  • GEOADD:添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member
  • GEODIST:计算指定的两个点之间的距离并返回
  • GEOHASH:将指定member的坐标转为hash字符串形式并返回
  • GEOPOS:返回指定member的坐标
  • GEORADIUS:指定圆心、半径,找到该圆内包含的所有member,并按照与圆心之间的距离排序后返回。6.以后已废弃
  • GEOSEARCH:在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2.新功能
  • GEOSEARCHSTORE:与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key。 6.2.新功能

BitMap数据结构的基本用法

BitMap的操作命令有:

  • SETBIT:向指定位置(offset)存入一个0或1
  • GETBIT :获取指定位置(offset)的bit
  • BITCOUNT :统计BitMap中值为1的bit位的数量
  • BITFIELD :操作(查询、修改、自增)BitMapbit数组中的指定位置(offset)的值
  • BITFIELD_RO :获取BitMapbit数组,并以十进制形式返回
  • BITOP :将多个BitMap的结果做位运算(与 、或、异或)
  • BITPOS :查找bit数组中指定范围内第一个0或1出现的位置
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值