Redis

文章目录

一、认识NoSql

1.NoSql-键值数据库

在这里插入图片描述

2.关系型数据库与非关系型数据库区别

在这里插入图片描述

二、Redis

1.概念

Redis诞生于2009年全称是Remote Dictionary Server,远程词典服务器,是一个基于内存的键值型NoSQL数据库。

2.特征

1.键值(key-value)型,value支持多种不同数据结构,功能丰富

2.单线程,每个命令具备原子性

3.低延迟,速度快(基于内存、IO多路复用、良好的编码)。

4.支持数据持久化

5.支持主从集群、分片集群

6.支持多语言客户端

3.Redis数据结构

Redis是一个key-value的数据库,key一般是string类型,value为以下类型

1.String类型

1.1 概念
String类型,也就是字符串类型,是Redis中最简单的存储类型。不过根据字符串的格式不同,又可以分为3类:

1.string:普通字符串

2.int:整数类型,可以做自增、自减操作

3.float:浮点类型,可以做自增、自减操作

不管是哪种格式,底层都是字节数组形式存储,只不过是编码方式不同。字符串类型的最大空间不能超过512m.

在这里插入图片描述

1.2 常见命令
1. SET:添加或者修改已经存在的一个String类型的键值对

2. GET:根据key获取String类型的value

3. MSET:批量添加多个String类型的键值对

4. MGET:根据多个key获取多个String类型的value

5. INCR:让一个整型的value自增1

6. INCRBY:让一个整型的value自增并指定步长,例如:incrby num 2 让num的值自增2

7. INCRBYFLOAT:让一个浮点类型的数字自增并指定步长

8. SETNX:添加一个String类型的键值对:key不存在,则执行添加操作并返回1表示添加成功;key存在,则不执行.

9. SETEX:添加一个String类型的键值对,并且指定有效期
1.3 应用场景
1. 短信登录
将验证码存储到Redis中,并设置过期时间,从而实现通过验证码登录功能

1.发送验证码并实现注册/登录功能
在这里插入图片描述
2.登录后每次请求校验登录状态
在这里插入图片描述

2. Hash类型

2.1 概念
Hash类型,也叫散列,其value是一个无序字典,类似于Java中的HashMap结构,常用来存储对象

在这里插入图片描述

2.2 常见命令
1. HSET key field value:添加或者修改hash类型key的field的值

2. HGET key field:获取一个hash类型key的field的值

3. HMSET:批量添加多个hash类型key的field的值

4. HMGET:批量获取多个hash类型key的field的值

5. HGETALL:获取一个hash类型的key中的所有的field和value

6. HKEYS:获取一个hash类型的key中的所有的field

7. HVALS:获取一个hash类型的key中的所有的value

8. HINCRBY:让一个hash类型key的字段值自增并指定步长

9. HSETNX:添加一个hash类型的key的field值,前提是这个field不存在,否则不执行
2.3 应用场景
保存用户信息或者其他对象信息

3. List类型

3.1 概念
Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。

特征也与LinkedList类似:

有序、元素可以重复、插入和删除快、查询速度一般
3.2 常见命令
1.LPUSH key  element ... :向列表左侧插入一个或多个元素

2.LPOP key:移除并返回列表左侧的第一个元素,没有则返回nil

3.RPUSH key  element ... :向列表右侧插入一个或多个元素

4.RPOP key:移除并返回列表右侧的第一个元素

5.LRANGE key star end:返回一段角标范围内的所有元素

6.BLPOP和BRPOP:与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil

在这里插入图片描述

3.2 应用场景
常用来存储一个有序数据-朋友圈点赞列表、评论列表.

4. Set类型

4.1 概念
1. Redis的Set结构与Java中的HashSet类似,可以看做是一个value为null的HashMap。

2. 因为也是一个hash表,因此具备与HashSet类似的特征:

无序、元素不可重复、查找快、支持交集、并集、差集等功能
4.2 常见命令
1. SADD key member ... :向set中添加一个或多个元素

2. SREM key member ... : 移除set中的指定元素

3. SCARD key: 返回set中元素的个数

4. SISMEMBER key member:判断一个元素是否存在于set中

5. SMEMBERS:获取set中的所有元素

6. SINTER key1 key2 ... :求key1与key2的交集

7. SDIFF key1 key2 ... :求key1与key2的差集

8. SUNION key1 key2 ..:求key1和key2的并集

在这里插入图片描述

4.3 应用场景

判断集合中是否包含某一个元素—判断订单中是否包含用户id(用户是否已下单)

5. SortedSet类型

5.1 概念
1. Redis的SortedSet是一个可排序的set集合,与Java中的TreeSet有些类似,但底层数据结构却差别很大。

2. SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加 hash表。

SortedSet具备下列特性:

可排序、元素不重复、查询速度快
5.2 常见命令
1. ZADD key score member:添加一个或多个元素到sorted set ,如果已经存在则更新其score值

2. ZREM key member:删除sorted set中的一个指定元素

3. ZSCORE key member : 获取sorted set中的指定元素的score值

4. ZRANK key member:获取sorted set 中的指定元素的排名(从0开始)

5. ZCARD key:获取sorted set中的元素个数

6. ZCOUNT key min max:统计score值在给定范围内的所有元素的个数

7. ZINCRBY key increment member:让sortedSet中的指定元素自增,步长为指定的increment值

8. ZRANGE key min max:按照score排序后,获取指定排名范围内的元素

9. ZRANGEBYSCORE key min max:按照score排序后,获取指定score范围内的元素

10. ZDIFF、ZINTER、ZUNION:求差集、交集、并集

注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可
5.3 应用场景
因为SortedSet的可排序特性,经常被用来实现排行榜这样的功能

4. Redis客户端-SpringDataRedis

4.1 概念

SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis

官网地址:https://spring.io/projects/spring-data-redis

4.2 RedisTemplate工具类

SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。

在这里插入图片描述

4.3 SpringBoot集成Redis

1. 引入依赖
<!--Redis依赖-->
<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>
2. 配置文件

在这里插入图片描述

5.Redis缓存

5.1 概念

缓存就是数据交换的缓冲区(称作Cache [ kæʃ ] ),是存贮数据的临时地方,一般读写性能较高。

5.2 缓存的优点和缺点

在这里插入图片描述

5.3 Demo-查询商品缓存实现流程

在这里插入图片描述

5.4 缓存更新策略

1. 三种策略说明

在这里插入图片描述

2. 应用场景
1. 低一致性需求:使用内存淘汰机制。例如商品类型的查询缓存

2. 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如商品详情查询的缓存
3. 主动更新策略解析
3.1 删除缓存还是更新缓存?
更新缓存:每次更新数据库都更新缓存,无效写操作比较多

删除缓存:更新数据库时删除缓存,查询时再更新缓存
3.2 如何保证缓存与数据库的操作-同时成功或失败?
单体系统,将缓存与数据库操作放在一个事务

分布式系统,利用TCC等分布式事务方案
3.3 先操作缓存还是先操作数据库?
先操作数据库,再删除缓存

先删除缓存,再操作数据库
3.4 流程图模拟两个线程读写操作
线程1:读操作

线程2:写操作

在这里插入图片描述

4. 总结
1. 低一致性需求:使用Redis自带的内存淘汰机制

2. 高一致性需求:主动更新,并以超时剔除作为兜底方案

读操作:

	缓存命中则直接返回

	缓存未命中则查询数据库,并写入缓存,设定超时时间

写操作:

	先写数据库,然后再删除缓存

	要确保数据库与缓存操作的原子性

5.5 缓存常见问题

1.缓存穿透
1.1 概念

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库

1.2 解决方案
1. 缓存空对象-将key缓存到redis中
优点:实现简单,维护方便

缺点:额外的内存消耗,可能造成短期的不一致

在这里插入图片描述

2. 布隆过滤器-将key转换成二进制保存到字节数组中
利用布隆过滤算法,在请求进入Redis之前先判断是否存在,不存在则直接拒绝请求

优点:内存占用少,没有多余key

缺点:实现复杂,存在误判可能

在这里插入图片描述

1.3 其他解决方案
增强id的复杂度,避免被猜测id规律

做好数据的基础格式校验

加强用户权限校验

做好热点参数的限流
2. 缓存雪崩
2.1 概念

在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

在这里插入图片描述

2.2 解决方案
1. 给不同的Key的TTL添加随机值

2. 利用Redis集群提高服务的可用性

3. 给缓存业务添加降级限流策略

4. 给业务添加多级缓存
3. 缓存击穿
3.1 概念

热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
在这里插入图片描述

3.2 解决方案
1. 互斥锁

为重建缓存数据业务加锁,使得只有一个线程来执行该业务,其他线程获取锁失败,处于阻塞状态,从而降低数据库访问压力.
在这里插入图片描述

2. 逻辑过期

为热点数据设置逻辑过期时间,意味着任何线程访问该数据都是可以命中的.如果该数据逻辑时间已过期:

线程获取互斥锁成功后,开启新线程来执行缓存重建业务.

线程获取互斥锁失败,返回过期数据。
在这里插入图片描述
在这里插入图片描述

3.3 两种方案优缺点

在这里插入图片描述

3.4 互斥锁实现
命令: setnx key value expiretime

方法: setIfAbsent(String key,String value,long timeout,TimeUnit unit)

在这里插入图片描述

3.5 逻辑过期实现

在这里插入图片描述

6. 优惠券秒杀

6.1 全局唯一ID

0. 场景

随着业务的发展,数据或者 ID 的生成会在不同的地方,比如常见的数据的分库分表,在分库分表的情况下,假如不同的表生成的 ID 不能保证唯一性,那么就会导致一个 ID 不能保证一条数据的唯一性,就会带来问题。
所以分布式 ID 的主要目的:不论何时何地,生成的 ID 都是唯一的。

1. 概念
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具
2. 特性

在这里插入图片描述

3. 全局唯一ID生成策略
1. UUID

2. Redis自增

3. snowflake算法

4. 数据库自增
 
5. Mysql 的全局 ID 生成表
4. Redis自增ID策略
1. 每天一个key,方便统计订单量

2. ID构造是 时间戳 + 计数器

6.2 实现优惠券秒杀下单

1. 概念

在这里插入图片描述

2. 注意事项

在这里插入图片描述

6.3 超卖问题

1. 概念

高并发场景下,扣减商品库存可能会出现超卖问题,导致订单为负数,从而产生业务问题
在这里插入图片描述

2. 解决方案-悲观锁
2.1 概念
认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。

例如、Lock都属于悲观锁
2.2 实现方式-Synchronized
为可能产生线程安全的代码或者代码块添加该关键字
2.3 实现方式-Lock
同上-注意需要手动释放锁
3. 解决方案-乐观锁
3.1 概念
认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。

如果没有修改则认为是安全的,自己才更新数据。

如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常

乐观锁的关键是判断之前查询到的数据是否有被修改过

3.2 实现方式-版本号法
为数据添加一个版本号字段,通过判断该值是否发生变化来确认数据是否被修改

在这里插入图片描述

3.3 实现方式-CAS法(compare and set)
将共享数据作为判断条件,更新之前先查询

在这里插入图片描述

6.4 一人一单

1. 概念
修改秒杀业务,要求同一个优惠券,一个用户只能下一单

在这里插入图片描述

2. 实现-悲观锁-单机模式下适用;集群模式下不适用
2.1 同步方法-锁的是orderServiceImpl对象
在方法上加锁-锁的是方法所属的对象,我们的本意是当同一个用户多次下单时,保证多次下单这个操作是线程安全的;

不同用户之间下单仍然是并行操作,因为他们之间不存在线程安全问题。

而在方法上加锁,导致不同用户在获取该对象时,为了保证线程安全,变成了串行操作.大大降低了执行效率
	// 秒杀优惠券业务实现
	@Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足!");
        }
		// 判断订单是否存在,存在则说明用户已下单.
        return createVoucherOrder(voucherId);
    }

 @Transactional
 public Synchronized Result createVoucherOrder(Long voucherId) {
        // 5.一人一单
        Long userId = UserHolder.getUser().getId();
        // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
         // 5.2.判断是否存在
          if (count > 0) {
              // 用户已经购买过了
              return Result.fail("用户已经购买过一次!");
          }

         // 6.扣减库存
         boolean success = seckillVoucherService.update()
                 .setSql("stock = stock - 1") // set stock = stock - 1
                 .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                 .update();
         if (!success) {
              // 扣减失败
              return Result.fail("库存不足!");
          }
         // 7.创建订单
         VoucherOrder voucherOrder = new VoucherOrder();
         // 7.1.订单id
         long orderId = redisIdWorker.nextId("order");
         voucherOrder.setId(orderId);
         // 7.2.用户id
         voucherOrder.setUserId(userId);
         // 7.3.代金券id
         voucherOrder.setVoucherId(voucherId);
         save(voucherOrder);
         // 7.返回订单id
         return Result.ok(orderId);
        }

2.2 同步代码块-锁的是用户id
分析业务得出结论:我们锁定的对象应该是用户id,这样能够保证同一用户多次发起请求时,保证线程安全.

String.intern()方法:返回字符串对象的规范表示,从字符串池中获取.
 @Transactional
 public Result createVoucherOrder(Long voucherId) {
        // 5.一人一单
        Long userId = UserHolder.getUser().getId();

        synchronized (userId.toString().intern()) {
            // 5.1.查询订单
            int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
            // 5.2.判断是否存在
            if (count > 0) {
                // 用户已经购买过了
                return Result.fail("用户已经购买过一次!");
            }

            // 6.扣减库存
            boolean success = seckillVoucherService.update()
                    .setSql("stock = stock - 1") // set stock = stock - 1
                    .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                    .update();
            if (!success) {
                // 扣减失败
                return Result.fail("库存不足!");
            }

            // 7.创建订单
            VoucherOrder voucherOrder = new VoucherOrder();
            // 7.1.订单id
            long orderId = redisIdWorker.nextId("order");
            voucherOrder.setId(orderId);
            // 7.2.用户id
            voucherOrder.setUserId(userId);
            // 7.3.代金券id
            voucherOrder.setVoucherId(voucherId);
            save(voucherOrder);

            // 7.返回订单id
            return Result.ok(orderId);
        }
2.3 事务与并发
1.并发问题:分析上述代码发现,释放锁之后才执行事务提交操作,假设此时有其他线程执行秒杀业务,可能造成线程安全问题.所以我们应该提交事务后再释放锁.
@Override
public Result seckillVoucher(Long voucherId) {
	Long userId = UserHolder.getUser().getId();
	synchronized(userId.toString().intern()) {
		return createVoucherOrder(voucherId);
	}
}
2.事务问题:事务想要生效,需要通过当前类的代理类对象来实现事务操作,而我们在方法A中调用另一个方法B,拿到的是当前对象,事务不会生效.
@Override
public Result seckillVoucher(Long voucherId) {
	Long userId = UserHolder.getUser().getId();
	synchronized(userId.toString().intern()) {
	// 获取当前实现类的代理对象。
	IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentPorxy();
		return proxy.createVoucherOrder(voucherId);
	}
}

6.5 分布式锁

1. 概念
满足分布式系统或集群模式下多进程可见并且互斥的锁。

在这里插入图片描述

2. 特性在这里插入图片描述
3. 分布式锁的三种实现方式

在这里插入图片描述

4. 基于Redis的分布式锁
1. 实现分布式锁时需要实现的两个基本方法:
1.获取锁:

	互斥:确保只能有一个线程获取锁

	非阻塞:尝试一次,成功返回true,失败返回false

在这里插入图片描述
2.释放锁:

	手动释放
	
	超时释放:获取锁时添加一个超时时间

在这里插入图片描述

2. 流程梳理

在这里插入图片描述

5. 分布式锁误删问题
1.问题复现

1.假设线程1获取锁成功,然后执行自己的业务逻辑,如果业务执行时长大于获取锁时设置的过期时间。

2.此时锁被自动释放,线程2获取锁,执行业务

3.线程2在执行业务期间,线程1的业务执行完毕,线程1手动释放锁,而且释放的是线程2的锁

4.线程3获取锁成功,执行自己的业务逻辑

5.以上情况,会出现线程安全问题

在这里插入图片描述

2. 解决方法
1.在获取锁时存入线程标示(可以用UUID表示)

2.在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
	
	如果一致则释放锁

	如果不一致则不释放锁

在这里插入图片描述

3. 代码实现

3.1 获取锁时存入线程标示
在这里插入图片描述
3.2 释放锁时判断线程标示
在这里插入图片描述

6. 分布式锁的原子性问题
1. 问题复现

1.线程1获取锁时,加入线程标识,获取成功,执行业务逻辑

2.释放锁时先判断标示是否是自己的,然后执行释放操作

3.假设判断成功后,程序阻塞了,达到超时时间,锁自动释放.

4.线程2获取锁,并加入线程标识。

5.线程2执行业务逻辑,此时线程1阻塞结束,因为已经判断过了,所以执行释放锁操作。

6.线程3获取锁,以上步骤会产生线程安全问题-原因为:判断操作与删除操作不是原子性的

在这里插入图片描述

2. 解决方法-Lua脚本
  1. Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性

  2. 调用脚本API
    在这里插入图片描述

  3. 代码实现
    1.Resources目录下创建Lua脚本
    在这里插入图片描述

    2.编写脚本内容

-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) ==  ARGV[1]) then
    -- 释放锁 del key
    return redis.call('del', KEYS[1])
end
return 0

3.预先加载该脚本

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

4.调用Lua脚本

@Override
    public void unlock() {
        // 调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }
7.分布式锁优化
1.基于setnx实现的分布式锁存在下面的问题
  1. 不可重入:同一个线程无法多次获取同一把锁

  2. 不可重试:获取锁只尝试一次就返回false,没有重试机制

  3. 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患

  4. 主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现

2. 解决方法-Redisson

在这里插入图片描述

3. 可重入锁
3.1 可重入锁原理
  1. 通过hah类型的结构数据,field为线程标识,value为重入次数,在同一个线程标识下,+1或者-1来多次获取锁或者释放锁.当value为0时,表示该线程已经释放了当前锁对象.
3.2 可重入锁应用场景
  1. method1()方法获取锁成功后,执行业务方法method2()
  2. method2()方法需要先获取锁,获取成功后才能执行业务,最后释放锁,此时因为方法1和方法2为同一个线程中执行的,如果锁不能重入,则方法2获取锁会失败,导致无法执行业务.
  3. 可重入锁的数据结构为hash类型.field字段记录线程标识,value为锁计数器
    在这里插入图片描述
3.3 获取锁的Lua脚本

在这里插入图片描述

3.4 释放锁的Lua脚本

在这里插入图片描述

4.Redisson分布式锁原理

在这里插入图片描述

5. 总结

Redisson分布式锁原理:

可重入:利用hash结构记录线程id和重入次数

可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制

超时续约:利用watchDog,每隔一段时间(releaseTime / 3),重置超时时间

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值