Redis笔记

目录

Redis概述:

缓存数据的分类:

Redis特性:

Redis IO模型

单线程模型:

混合线程模型

多线程模型

单线程模型与多线程模型优缺点

Redis安装与启用 

安装gcc

安装Redis 

启动Redis

查看Redis是否启动成功

停止Redis

连接前的配置

绑定客户端ID

 关闭保护模式

设置密码 

禁止/重命名命令

Redis客户端

命令行客户端

图形界面客户端

Java代码客户端 

Redis命令 

Redis基本命令

心跳命令ping

select命令

Redis通用命令

key的格式

String类型

常见命令

Hash类型

常见命令

List类型

常见命令

Set类型

常见命令

Zset类型

常见命令

Java客户端

Jedis

Jedis连接池

StringDataRedis

快速入门​编辑

序列化方式

Redis实战 

短信登录

发送短信验证码

短信登录验证、注册

基于Session实现登录

使用Redis共享Session

        发送短信登录验证码

        短信登录验证、注册

        校验登录状态

        拦截器优化

商户查询缓存

根据商户id查询商铺缓存

将店铺分类缓存到Redis中

缓存更新策略

实现店铺缓存与数据库一致

缓存穿透

        解决根据id查询店铺信息缓存穿透问题

缓存雪崩

缓存击穿

        利用互斥锁解决缓存击穿问题

        利用逻辑过期解决缓存击穿问题

优惠卷秒杀

全局唯一ID

实现秒杀下单

关于超卖

一人一单

分布式锁

基于Redis实现分布式锁

基于Redis分布式锁优化

Redisson

         实现Redisson

Redisson可重入锁原理

秒杀优化 

stream消息队列 


Redis概述:

缓存数据的分类:

        实时同步数据:要求缓存中的数据必须与DB中的数据保持一致。如何保持?只要DB中的数据发生了变更,缓存中的数据立即消失。

        阶段性同步数据:其没有必要与DB中的数据保持一致,只要大差不差就行。如何实现?为缓存数据添加生存时长属性。

        缓存在使用的时候有一个预热的过程,就是提前加载一些常见的数据到缓存中。阶段性同步数据就可以在预热中进行缓存。

Redis特性:

  1.         性能极高:Redis读的速度可以达到11w次/s,写的速度可以可以达到8w次/s。之所以具有这么高的性能,因为有以下几点原因:1.Redis的所有操作都是在内存中发生的。2.Redis是用C语言开发的。3.Redis的源码非常精细。
  2.         简单稳定:Redis源码很少,早期版本只有2w行左右。从3.0版本开始,增加了集群功能,代码变为了5w行左右。
  3.         持久化:Redis内存中的数据可以进行持久化。其有两种方式:RDB与AOF。
  4.         高可用集群:Redis提供了高可用的主从集群功能,可以确保系统的安全性。
  5.         丰富的数据类型:Redis 是一个 key-value 存储系统。支持存储的 value 类型很多,包括String(字符串)、List(链表)、Set(集合)、Zset(sorted set --有序集合)和 Hash(哈希类型)
    等,还有 BitMap、HyperLogLog、Geospatial 类型。
      BitMap:一般用于大数据量的二值性统计。
      HyperLogLog:其是 Hyperlog Log,用于对数据量超级庞大的日志做去重统计。
      Geospatial:地理空间,其主要用于地理位置相关的计算
  6.         强大的功能:Redis提供了数据过期功能、发布/订阅功能、简单事务功能。还支持Lua脚本扩展功能。
  7.         客户端语言广泛:Redis提供了简单的 TCP 通信协议,编程语言可以方便地的接入 Redis。所以,有很多的开源社区、大公司等开发出了很多语言的 Redis 客户端。
  8.         支持ACL权限控制:之前的权限控制非常笨拙。从 Redis6 开始引入了 ACL 模块,可以
    为不同用户定制不同的用户权限。
    ACL,Access Control List,访问控制列表,是一种细粒度的权限管理策略,可以针对任意用户与组进行权限控制。目前大多数 Unix 系统与 Linux 2.6 版本已经支持 ACL 了。 Zookeeper 早已支持 ACL 了。
    Unix 与 Linux 系统默认使用是 UGO(User、Group、Other)权限控制策略,其是一种粗粒度的权限管理策略。
  9.         支持多线程IO模型:Redis 之前版本采用的是单线程模型,从 6.0 版本开始支持了多线
    程模型。

Redis IO模型

单线程模型:

        对于 Redis 3.0 及其以前版本,Redis 的 IO 模型采用的是纯粹的单线程模型。即所有客户端的请求全部由一个线程处理

混合线程模型

        从 Redis 4.0 版本开始,Redis 中就开始加入了多线程元素。处理客户端请求的仍是单线程模型,但对于一些比较耗时但又不影响对客户端的响应的操作,就由后台其它线程来处理。
例如,持久化、对 AOF 的 rewrite、对失效连接的清理等。

多线程模型

        Redis 6.0 版本,才是真正意义上的多线程模型。因为其对于客户端请求的处理采用的是多线程模型。

单线程模型与多线程模型优缺点

(1) 单线程模型
        优点:可维护性高,性能高。不存在并发读写情况,所以也就不存在执行顺序的不确定性,不存在线程切换开销,不存在死锁问题,不存在为了数据安全而进行的加锁/解锁开销。
        缺点:性能会受到影响,且由于单线程只能使用一个处理器,所以会形成处理器浪费。
(2) 多线程模型
        优点:其结合了多线程与单线程的优点,避开了它们的所有不足
        缺点:该模型没有显示不足。如果非要找其不足的话就是,其并非是一个真正意义上的“多线程”,因为真正处理“任务”的线程仍是单线程。所以,其对性能也是有些影响的。

Redis安装与启用 

安装gcc

        由于 Redis 是由 C/C++语言编写的,而从官网下载的 Redis 安装包是需要编译后才可安装的,所以对其进行编译就必须要使用相关编译器。对于 C/C++语言的编译器,使用最多的是gcc 与 gcc-c++,而这两款编译器在 CentOS7 中是没有安装的,所以首先要安装这两款编译器。
        GCC,GNU Compiler Collection,GNU 编译器集合。        
安装指令
        yum -y install gcc gcc-c++ 

安装Redis 

        官网下载redis安装包

        Linux上传安装包

                rz指令进行上传

        Linux解压安装包

                tar -zxvf redis安装包 -C 解压路径

        Linux安装Redis

                进入redis文件输入Linux指令        make        进行安装

                安装完后使用Linux指令        make install        进行安装

启动Redis

        <1>直接启动

                Linux指令        redis-server         这种方式有一个弊端,占用了命令行,当我们CTRL+C发现Redis也退出了

         <2>命令式后台启动

                Linux指令        nohup redis-server &        进行后台启动,启动成功后会在当前目录多一个文件

         <3>配置后台启动       

                需要配置redis.conf文件,Linux指令        vim redis.conf       

                输入        /        使用命令模式搜索daemonize,

                输入        i        使用输入模式,将no改为yes

                 输入        redis-server /opt/apps/redis/redis.conf

查看Redis是否启动成功

        Linux指令        ps aux | grep redis

停止Redis

        输入        redis-cli shutdown

        在redis里面输入shutdown

连接前的配置

绑定客户端ID

        需要配置redis.conf文件,Linux指令        vim redis.conf     

                输入        /        使用命令模式搜索bind,

                输入        i        使用输入模式,修改目标为以下格式

 关闭保护模式

        需要配置redis.conf文件,Linux指令        vim redis.conf     

                输入        /        使用命令模式搜索protected,

                输入        i        使用输入模式,将yes改为no

设置密码 

        需要配置redis.conf文件,Linux指令        vim redis.conf     

                输入        /        使用命令模式搜索requirepass,

                输入        i        使用输入模式,修改目标为以下格式,后面foobared修改为你想改的密码

        设置以后进入redis输入无法完成得先输入        auth 密码  

禁止/重命名命令

需要配置redis.conf文件,Linux指令        vim redis.conf     

                输入        /        使用命令模式搜索rename-command,

                输入        i        使用输入模式,修改目标为以下格式,表示禁止flushall命令

Redis客户端

命令行客户端

命令行启动客户端

        redis-cli -h IP地址 -p 端口号 -a 密码

                -h输入客户端的IP地址

                -p端口号,一般为6379

                -a如果设置了密码,这里得输入密码

如果是本机,无需-h,如果端口号是6379无需-p,如果无密码无需-a

图形界面客户端

        1.Redis Desktop Manager

                官网为:https://resp.app/(原来是 http://redisdesktop.com)。

        2.RedisPlus

                RedisPlus 的官网地址为 https://gitee.com/MaxBill/RedisPlus。

Java代码客户端 

        所谓 Java 代码客户端就是一套操作 Redis 的 API,其作用就像 JDBC 一样,所以 Java 代码客户端其实就是一个或多个 Jar 包,提供了对 Redis 的操作接口。

        对 Redis 操作的 API 很多,例如 jdbc-redis、jredis 等,但最常用也是最有名的是 Jedis。

Redis命令 

Redis基本命令

心跳命令ping

        输入ping命令,会看到pong响应,说明该客户端与Redis的连接时正常的,该命令成为心跳命令

select命令

        Redis 默认有 16 个数据库。这个在 Redis Desktop Manager(RDM)图形客户端中可以直
观地看到。
        用select来切换数据库
示例:
        select 3
        含义是切换到3号数据库

Redis通用命令

        通用指令是部分数据类型的,都可以使用的指令,常见的有

                KEYS:查看符合模板的所有key

                DEL :删除一个指定的key

                EXISTS:判断key是否存在

                EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除

                TTL:查看一个key的剩余有效期

key的格式

        key允许有多个单词形成层级结构,当额单词之间用“:”隔开,格式如:

                项目名:业务名:类型:

        这个格式并非固定,也可以根据自己的需求来删除或添加词条

        例如:

                user相关: admin:user:1

                product相关:admin:product:1

String类型

        string类型,也就是字符串类型,是Redis最简单的存储类型,其value是字符串,不过根据字符串的格式不同,又可以分为3类:

                string:普通字符串

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

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

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

常见命令

        SET:添加或修改已经存在的一个String类型的键值对

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

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

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

        INCR:让一个整形的key自增1

        INCRBY:让一个整形的key自增并指定步长

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

        SETNX:添加一个String类型的键值对,前提是这个key不存在,否则不执行

        SETEX:添加一个String类型 的键值对,并且指定有效期

Hash类型

        hash类型也叫散列,其value是一个无序字典,类似于Java中的HashMap结构

常见命令

        HSET key field value:添加或者修改hash类型key的field的值

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

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

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

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

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

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

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

        HSETNX:添加一个hash类型的key的field值,前提是这个field不存在,否则不执行

List类型

        list类型与Java中的LinkedList类似,可以看作一个双向链表结构。既可以支持正向检索,也可以支持反向检索。特征也与LinkedList类似:

  •                 有序
  •                 元素可以重复
  •                 插入和删除快
  •                 查询速度一般

        常用来存储一个有序数据,例如:朋友圈点赞列表,评论列表等。

常见命令

        LPUSH key element  . . . :向列表左侧插入一个或多个元素

        LPOP key:移除并返回列表左侧的第一个元素

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

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

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

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

Set类型

        set类型与Java中的HashSet类似,可以看作一个value为null的HashMap。也可以是一个hash表,因此具备与HashSet类似的特征

  •                 无序
  •                 元素不可重复
  •                 查找快
  •                 支持交集并集差集等功能

常见命令

        SADD key member . . . :向set中添加一个或多个元素

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

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

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

        SMEMBERS:获取set中的所有元素

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

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

        SUNION key1 key2 . . . :求key1与key2的并集

Zset类型

        是一个可排序的set集合,与Java中的TreeSet类似,但底层数据结构却差别很大。Zset中的每一个元素都带一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加hash表,具有以下特性

  •                 可排序
  •                 元素不可重复
  •                 查询速度快

常见命令

        ZADD key score member:添加一个或多个元素到Zset,如果寂静存在则更新其score值

        ZREM key member:删除Zset中的一个指定元素

        ZSCORE key member:获取Zset中指定元素的score值

        ZRANK key member:获取Zset中指定元素的排名

        ZCARD key:获取Zset中的元素个数

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

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

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

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

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

注:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可

Java客户端

Jedis

        导入依赖GitHub - redis/jedis: Redis Java client designed for performance and ease of use.

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>4.3.0</version>
        </dependency>

         导入junit

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.9.1</version>
            <scope>test</scope>
        </dependency>

1.进入test,引入Jedis,

  1. 建立与Reids的链接,
  2. 输入密码,
  3. 选择数据库

2.调用Jedis,使用Redis命令

3.释放资源

public class jedis {
    private Jedis jedis;

    @BeforeEach
    void setUp() {
        //1.建立连接
       jedis = new Jedis("192.168.80.135", 6379);
        //2.设置密码
        //jedis.auth("");
        //3.选择数据库
        jedis.select(0);
    }
    @Test
    void testString(){
        String setnames = jedis.set("name", "wangwu");
        System.out.println(setnames);
        String getname = jedis.get("name");
        System.out.println(getname);
    }
    @AfterEach
    void tearDown(){
        if (jedis!=null){
            jedis.close();
        }
    }

}

Jedis连接池

        Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此推荐使用Jedis连接池代替Jedis的直连方式。

public class JedisConnectionFactory {
    private static final JedisPool jedisPool;

    static {
        //配置连接池
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        //最大连接数
        poolConfig.setMaxTotal(8);
        //最大空闲连接
        poolConfig.setMaxIdle(8);
        //最小空闲连接
        poolConfig.setMinIdle(0);
        //设置最长等待时间
        poolConfig.setMaxWaitMillis(1000);
        //创建连接池对象
        jedisPool = new JedisPool(poolConfig, "192.168.80.135", 6379, 1000);

    }

    public static Jedis getJedis() {
        return jedisPool.getResource();
    }
}

StringDataRedis

        SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis,官网地址:https://spring.io/projects/spring-data-redis

  • 提供了对不同Redis客户端的整合(Lettuce和Jedis)
  • 提供了RedisTemplate统一API来操作Redis 支持Redis的发布订阅模型
  • 支持Redis哨兵和Redis集群
  • 支持基于Lettuce的响应式编程
  • 支持基于JDK、JSON、字符串、Spring对象的数据序列化及反序列化
  • 支持基于Redis的JDKCollection实现

快速入门

 1.依赖,有StringDataRedis依赖以及连接池的依赖(idea版本为2.5.7)

<!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
<!--redis连接池-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

2.设置Reids配置(我是用的yml格式)

spring:
  redis:
    port: 6379
    host: 192.168.80.135
    lettuce:
      pool:
        max-wait: 1000
        max-idle: 8
        max-active: 8
        min-idle: 0

3.自动注入redistemplate

    @Autowired
        private RedisTemplate redisTemplate;
    @Test
    void contextLoads() {
        redisTemplate.opsForValue().set("name","wangwu");
        Object name = redisTemplate.opsForValue().get("name");
        System.out.println(name);
    }

序列化方式

        RedisTemplate可以接收任意Object作为值写入Redis,只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化。缺点是

  •                 可读性差
  •                 内存占用较大

        采用自定义的序列化方式


@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        //创建RedisTempalte对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        //设置连接工厂
        template.setConnectionFactory(connectionFactory);
        //创建JSON序列化工具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        //设置Key的序列化
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        // 设置Value的序列化
        template.setValueSerializer(jsonRedisSerializer);
        template.setHashValueSerializer(jsonRedisSerializer);
        // 返回
        return template;
    }
}

在使用序列化的时候要加入jackson依赖

<!--Jackson依赖-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

        使用自动序列化有一个问题,储存的Redis数据里面夹带了反序列化时需要的对象路径的数据,这个数据有时候比储存的数据还要大,Redis数据储存在内存中,这样会导致浪费空间。

         为了节省内存空间,我们不会使用JSON序列化器来处理value,而是同意使用String序列化器,要求只能存储String类型的key和value。当需要存储Java对象时,手动完成对象的序列化和反序列化

         String默认提供了一个StringRedisTemplare类,它的key和value的序列化方式默认就是String方式。省区了我们自定义RedisTemplate的过程


@SpringBootTest
class StringRedisApplicationTests {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    void contextLoads() {
        stringRedisTemplate.opsForValue().set("name", "wangwu");
        Object name = stringRedisTemplate.opsForValue().get("name");
        System.out.println(name);
    }
    private static final ObjectMapper mapper=new ObjectMapper();
    @Test
    void user() throws JsonProcessingException {
//        stringRedisTemplate.opsForValue().set("user:name",new User("lisi",21));
//        User user = (User) stringRedisTemplate.opsForValue().get("user:name");
//        System.out.println("user = " + user);
        //创建对象
        User user = new User("lisi",31);
        //手动序列化
        String userjson = mapper.writeValueAsString(user);
        //导入数据
        stringRedisTemplate.opsForValue().set("user:name:2",userjson);
        //获取数据
        String usergetjson = stringRedisTemplate.opsForValue().get("user:name:2");
        //手动反序列化
        User user1 = mapper.readValue(usergetjson,User.class);
        System.out.println("user1 = " + user1);
    }

}

Redis实战 

短信登录

发送短信验证码

  1. 校验手机号是否符合正确的手机号的格式,采用正则表达式
    1. 校验失败返回失败信息
  2. 校验成功生成验证码
  3. 保存验证码
  4. 发送验证码
 public Result sendCode(String phone, HttpSession session) {
        // 1.验证手机号
        boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);
        log.debug("手机号验证{}", phoneInvalid);
        if (phoneInvalid) {
            // 2.验证失败
            return Result.fail("手机号格式不对");
        }
        // 3.验证成功生成验证码
        String code = RandomUtil.randomNumbers(6);
        log.debug("生成的验证码{}", code);
        // 4.保存验证码到session
        session.setAttribute("code", code);
        // 5.发送验证码
        log.debug("验证码发送成功,验证码{}", code);
        // 6.返回
        return Result.ok();
}

短信登录验证、注册

  1. 验证手机号格式是否正确(每一步都要验证一次)
    1. 格式错误,返回错误信息
  2. 比对验证码是否相同
    1. 不相同,返回错误信息
  3.  相同,通过手机号查找用户是否存在
    1. 不存在,创建新用户
  4. 保存用户到Session
    /**
     * 登录验证
     *
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String phone = loginForm.getPhone();
        // 检验手机号是否是正确格式
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 格式错误返回错误信息
            log.debug("登录验证手机号格式不正确");
            return Result.fail("您输入的手机号格式不正确");
        }
        String loginCode = loginForm.getCode();
        // 根据手机号获取Redis里面的code
        String RedisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        // 比对验证吗是否一致
        if (loginCode == null || !RedisCode.equals(loginCode)) {
            // 比对失败返回错误信息
            log.debug("登录验证码不正确");
            return Result.fail("您未输入验证码或您输入的验证码不正确");
        }
        // 验证该手机号用户是否存在
        User user = query().eq("phone", phone).one();
        if (user == null) {
            // 不存在创建新用户
            user = createUserWithPhone(phone);
        }

        // 保存用户到Redis
        // 生成随机的token当作令牌
        String token = UUID.randomUUID().toString(true);
        // 将user复制给UserDto,目的是不要将用户的全部信息返回给前端
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        // 将user对象转换为map集合
        Map<String, Object> UserMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),CopyOptions
                .create()
                .setIgnoreNullValue(true)
                .setFieldValueEditor((fieldName,fieldaValue)->fieldaValue.toString()));
        // 保存到redis中
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,UserMap);
        // 设置登录过期时间
        stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES);
        return Result.ok(token);
}
    /**
     * 创建新用户
     * @param phone
     * @return
     */
    private User createUserWithPhone(String phone) {
        // 创建User
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(5));
        // 保存用户
        save(user);
        log.debug("用户保存成功");
        return user;
}

基于Session实现登录

        登陆前要进行拦截,来判断用户的账号是否存在,以及一些需要登录才能访问的页面也需要拦截器进行拦截。

  1. 编写拦截器
    1. 获取session
    2. 获取session中的用户
    3. 判断用户是否存在
      1. 不存在,拦截,返回状态码401
    4. 保存用户信息到ThreadLocal
    5. 放行
  2. 开启拦截器
public class LoginInterceptorXiang implements HandlerInterceptor {
    /**
     * 在control前进行
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取session
        HttpSession session = request.getSession();
        // 获取session里的用户
        Object user = session.getAttribute("user");
        // 判断用户是否存在
        if (user == null) {
            response.setStatus(401);
            return false;
        }
        // 不存在返回错误状态
        // 存在保存用户到ThreadLocald
        UserHolder.saveUser((UserDTO) user);
        return true;
    }

    /**
     * 在control方法后进行
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
//    @Override
//    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
//    }

    /**
     * 在渲染后进行
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
       UserHolderXiang.removeUser();
    }
}
@Configuration
public class MvcConfigXiang implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptorXiang())
                .excludePathPatterns(
                   
                        "/user/code",
                        "/user/login"
                );
    }
}

使用Redis共享Session

        发送短信登录验证码

  1. 校验手机号是否符合正确的手机号的格式,采用正则表达式
    1. 校验失败返回失败信息
  2. 生成随机验证码
  3. 将验证码作为String类型存入Redis,key是手机号,value是验证码
  4. 发送验证码
    /**
     * 验证手机号格式是否正确
     *
     * @param phone
     * @param session
     * @return
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 验证手机号

        if (RegexUtils.isPhoneInvalid(phone)) {
            // 手机号不符合规范
            log.debug("手机号格式不对");
            return Result.fail("手机号格式不对");
        }

        // 手机号符合规范,生成验证码
        String code = RandomUtil.randomNumbers(6);
        log.debug("生成的验证码{}", code);
        // 保存验证码到redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TIME, TimeUnit.MINUTES);

        // 发送验证码d
        log.debug("发送验证码成功,验证码{}", code);
        return Result.ok();
}

        短信登录验证、注册

  1. 验证手机号格式是否正确
    1. 格式错误,返回错误信息
  2. 根据手机号取出Redis里面存储的验证码
  3. 比对验证码是否与输入的验证码一致
    1. 比对失败,返回错误信息
  4. 比对成功,验证该手机号用户是否存在
    1. 不存在,创建新用户,保存到数据库
  5. 存在,保存用户到Redis里面(这里用到一个时间,30分钟将会自动删除Redis用户,以此来模仿Session30分钟没操作自动退出登录)
    1. 生成随机的token当作令牌
    2. 将User复制给UserDto,目的是不要将用户的全部信息返回给前端
    3. 将User对象转换为Map集合
    4. 保存到Redis中(key为token,value为map集合)
    5. 设置登录有效期
    /**
     * 登录验证
     *
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String phone = loginForm.getPhone();
        // 检验手机号是否是正确格式
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 格式错误返回错误信息
            log.debug("登录验证手机号格式不正确");
            return Result.fail("您输入的手机号格式不正确");
        }
        String loginCode = loginForm.getCode();
        // 根据手机号获取Redis里面的code
        String RedisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        // 比对验证吗是否一致
        if (loginCode == null || !RedisCode.equals(loginCode)) {
            // 比对失败返回错误信息
            log.debug("登录验证码不正确");
            return Result.fail("您未输入验证码或您输入的验证码不正确");
        }
        // 验证该手机号用户是否存在
        User user = query().eq("phone", phone).one();
        if (user == null) {
            // 不存在创建新用户
            user = createUserWithPhone(phone);
        }

        // 保存用户到Redis
        // 生成随机的token当作令牌
        String token = UUID.randomUUID().toString(true);
        // 将user复制给UserDto,目的是不要将用户的全部信息返回给前端
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        // 将user对象转换为map集合
        Map<String, Object> UserMap = BeanUtil.beanToMap(userDTO);
        // 保存到redis中
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,UserMap);
        // 设置登录过期时间
        stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES);
        return Result.ok(token);
}
    /**
     * 创建新用户
     *
     * @param phone
     * @return
     */
    private User createUserWithPhone(String phone) {
        // 创建User
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(5));
        // 保存用户
        save(user);
        log.debug("用户保存成功");
        return user;
    }

        校验登录状态

  • 编写拦截器
    • 获取请求投中的token
    • 如果token不存在,进行拦截,返回状态码401
    • 根据Token获取Redis中的用户
    • 判断用户是否存在
    • 不存在,进行拦截,返回状态码401
    • 将查询到的Hash数据转化为UserDto对象
    • 将对象保存到ThreadLocal中
    • 自动延长登录有效期
  1. 开启拦截器
public class LoginInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取请求投中的token
        String token = request.getHeader("authorization");

        if (token == null) {
            //如果token不存在,进行拦截,返回状态码401
            response.setStatus(401);
            return false;
        }
        //根据Token获取Redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);

        //判断用户是否存在
        if (userMap == null) {
            //不存在,进行拦截,返回状态码401
            response.setStatus(401);
            return false;
        }

        //将查询到的Hash数据转化为UserDto对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //将对象保存到ThreadLocal中
        UserHolder.saveUser(userDTO);
        //自动延长登录有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL, TimeUnit.MINUTES);

        return true;
    }
}
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        // token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

        拦截器优化

        在以上的拦截器前面再加一层拦截器,原因是,在原来的拦截器中,自动延长用户登录时间的时候,只有当用户访问了需要登录拦截的时候才会延长,再加一个拦截器,就可以做到只要用户有操作就可以延长登陆时间

public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 2.基于TOKEN获取redis中的用户
        String key  = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 5.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

public class LoginInterceptor implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            // 没有,需要拦截,设置状态码
            response.setStatus(401);
            // 拦截
            return false;
        }
        // 有用户,则放行
           return true;

    }}

        order属性是可以规定那个拦截器先执行(越小越先执行) 

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        // token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

商户查询缓存

根据商户id查询商铺缓存

  1. 提交商铺id
  2. 从Redis里面查询商铺缓存
  3. 判断缓存是否存在
    1. 存在,返回缓存数据,得到商铺信息(返回的时候要记得反序列化,因为缓存查到的数据是JSON数据)
  4. 不存在,根据商铺id进入数据库查询
    1. 数据库不存在,返回错误信息
  5. 存在,将商铺信息存储在Redis中,建立该商铺缓存(将商铺信息进行序列化)
  6. 返回商铺信息

    @Override
    public Result queryById(Long id) {


        //从Redis里面查询商铺缓存
        String cacheShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_XIANG + id);
        //判断缓存是否存在
        if (StrUtil.isNotBlank(cacheShop)) {
            //存在,返回缓存数据,得到商铺信息
            Shop shop = JSONUtil.toBean(cacheShop, Shop.class);
            return Result.ok(shop);
        }
        //不存在,根据商铺id进入数据库查询
        Shop shop = query().eq("id", id).one();
        //数据库不存在,返回错误信息
        if (shop==null) {
            return Result.fail("查询的商铺不存在");
        }
        //存在,将商铺信息存储在Redis中,建立该商铺缓存
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_XIANG + id, JSONUtil.toJsonStr(shop));
        //返回商铺信息
        return Result.ok(shop);
    }

将店铺分类缓存到Redis中

  1. 查询Redis的分类信息(按照分类排序)
    1. 存在,返回分类信息
  2. 不存在取数据库里查找
    1. 不存在,返回分类不存在
  3. 存在,将分类信息缓存到Redis
  4. 返回分类信息
 @Override
    public Result queryTypeList() {
        //查询Redis的分类信息(按照分类排序)
        String shopTypeJSON = stringRedisTemplate.opsForValue().get(CACHE_SHOPTYPE);
        //存在,返回分类信息
        if (StrUtil.isNotBlank(shopTypeJSON)) {
            List<ShopType> shopType = JSONUtil.toList(shopTypeJSON, ShopType.class);
            return Result.ok(shopType);
        }
        //不存在取数据库里查找
        List<ShopType> shopTypesList = query().orderByAsc("sort").list();
        //不存在,返回分类不存在
        if (shopTypesList==null){
                      return Result.fail("分类信息不存在");
        }
        //存在,将分类信息缓存到Redis
        shopTypeJSON = JSONUtil.toJsonStr(shopTypesList);
        stringRedisTemplate.opsForValue().set(CACHE_SHOPTYPE,shopTypeJSON);
        //返回分类信息
        return Result.ok(shopTypesList);
    }

缓存更新策略

操作缓存和数据库时由三个问题需要考虑

  • 删除缓存还是更新缓存
    • 更新缓存:每次更新数据库都更新缓存,无效写操作较多
    • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存(一般采取这个的比较多)
  • 如何保证缓存与数据库的操作的同时成功或失败
    • 单体系统:将缓存与数据库操作放在一个事务
    • 分布式系统:利用TCC等分布式事务方案
  • 先操作缓存还是先操作数据库
    • 先删除缓存,再操作数据库
    • 先操作数据库,再删除缓存(一般采取这个的比较多)

缓存更新策略的最佳实践方案:

  1. 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
  2. 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存
  • 读操作
    • 缓存命中则直接返回
    • 缓存未命中则查询数据库,并写入缓存,设定超时时间
  • 写操作
    • 先写数据库,然后再删除缓存
    • 要确保数据库与缓存操作的原子性

实现店铺缓存与数据库一致

  1. 在加入Redis缓存的时候加入过期时间
  2. 更新店铺信息时,先更新数据库,再删除缓存
    @Override
    public Result queryById(Long id) {


        //从Redis里面查询商铺缓存
        String cacheShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_XIANG + id);
        //判断缓存是否存在
        if (StrUtil.isNotBlank(cacheShop)) {
            //存在,返回缓存数据,得到商铺信息
            Shop shop = JSONUtil.toBean(cacheShop, Shop.class);
            return Result.ok(shop);
        }
        //不存在,根据商铺id进入数据库查询
        Shop shop = query().eq("id", id).one();
        //数据库不存在,返回错误信息
        if (shop == null) {
            return Result.fail("查询的商铺不存在");
        }
        //存在,将商铺信息存储在Redis中,建立该商铺缓存,加入TTL
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_XIANG + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //返回商铺信息
        return Result.ok(shop);
    }
    @Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("店铺不存在");
        }
        //先更新数据库
        updateById(shop);
        //再删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_XIANG + shop.getId());

        return Result.ok();
    }

缓存穿透

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

常见的解决方案有两种

  • 缓存空对象
    • 优点:实现简单,维护方便
    • 缺点:
    •         额外的内存消耗
    •         可能造成短期的不一致

     

  • 布隆过滤
    • 优点:内存占用较少,没有多余KEY
    • 缺点:
    •         实现复杂
    •         存在误判可能

     

  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

根据id查询店铺信息有缓存穿透的风险

        解决根据id查询店铺信息缓存穿透问题

 

  1. 提交商铺id
  2. 从Redis里面查询商铺缓存
  3. 判断缓存是否存在
    1. 存在,返回缓存数据,得到商铺信息(返回的时候要记得反序列化,因为缓存查到的数据是JSON数据)
  4. 不存在但是不等于NULL
    1. 返回错误信息
  5. 不存在,根据商铺id进入数据库查询
    1. 不存在,将空值写入Redis(空值为 "" )
  6. 存在,将商铺信息存储在Redis中,建立该商铺缓存(将商铺信息进行序列化)
  7. 返回商铺信息

    @Override
    public Result queryById(Long id) {
        //从Redis里面查询商铺缓存
        String cacheShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_XIANG + id);
        //判断缓存是否存在
        if (StrUtil.isNotBlank(cacheShop)) {
            //存在,返回缓存数据,得到商铺信息
            Shop shop = JSONUtil.toBean(cacheShop, Shop.class);
            return Result.ok(shop);
        }
        //不存在,但缓存不为NULL
        if (cacheShop != null) {
            return Result.fail("您要查找的店铺不存在");
        }
        //不存在,根据商铺id进入数据库查询
        Shop shop = query().eq("id", id).one();
        //数据库不存在,返回错误信息
        if (shop == null) {
            //不存在,将空值写入Redis中
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_XIANG + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return Result.ok(shop);
        }
        //存在,将商铺信息存储在Redis中,建立该商铺缓存,加入TTL
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_XIANG + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //返回商铺信息
        return Result.ok(shop);
    }

缓存雪崩

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

解决方案:

  1. 给不同的key的TTL添加随机值
  2. 利用Redis集群提高服务的高可用
  3. 给缓存业务添加降级限流策略
  4. 给业务添加多级缓存 

缓存击穿

        缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击

 

常见的解决方案有两种

  • 互斥锁

  • 逻辑过期

        利用互斥锁解决缓存击穿问题

  • 1.获取商铺id
  • 2.从Redis中查询商铺缓存
    • 2.1有缓存,直接返回数据
    • 2.2没有缓存,也不会NULL
  • 3尝试获取互斥锁
    • 3.1没有获取到,休眠一段时间
    • 3.2再次查询缓存是否存在
  • 4获取到互斥锁,根据店铺id查询数据库
  • 5判断数据库是否存在
    • 5.1数据库不存在,返回空
    • 5.2给Redis缓存一个 "" ,防止缓存穿透
  • 6数据库存在,将数据缓存到Redis中
    • 6.1释放互斥锁
  • 7返回数据
  /**
     * 依靠互斥锁解决缓存击穿问题
     *
     * @param id
     * @return
     */
    public Shop queryWithShopMutual(Long id) {
        //  1.获取商铺id
        //2.从Redis中查询商铺缓存
        String shopJSON = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //2.1有缓存,直接返回数据
        if (StrUtil.isNotBlank(shopJSON)) {

            Shop shop = JSONUtil.toBean(shopJSON, Shop.class);
            return shop;
        }
        //2.2没有缓存,也不会NULL
        if (shopJSON!=null){
            return null;
        }
            Shop shop=null;
        try {
            //3尝试获取互斥锁
            boolean tryLock = tryLock(LOCK_SHOP_KEY + id);
            //3.1没有获取到,休眠一段时间,再次查询缓存是否存在
            if (!tryLock) {
                Thread.sleep(30);
                return  queryWithShopMutual(id);
            }
            //4获取到互斥锁,根据店铺id查询数据库
             shop = query().eq("id", id).one();
            //5判断数据库是否存在
            //5.1数据库不存在,返回空
            if (shop == null) {
                stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            //6数据库存在,将数据缓存到Redis中
            String Shop2JSON = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, Shop2JSON, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
          throw  new RuntimeException(e);
        } finally {
            //6.1释放互斥锁
            unlock(LOCK_SHOP_KEY + id);
        }

        //7返回数据
        return shop;
    }
    /**
     * 尝试获得互斥锁
     * @param key
     * @return
     */
    public boolean tryLock(String key) {
        Boolean tryLock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 30, TimeUnit.MILLISECONDS);
        return BooleanUtil.isTrue(tryLock);
    }
    /**
     * 释放互斥锁
     * @param key
     */
    public void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

        利用逻辑过期解决缓存击穿问题

  • 1.获取商铺id
  • 2.从Redis中查询商铺缓存
  • 3.判断缓存是否存在
    • 3.1缓存不存在,返回空
    • 3.2缓存存在,需要判断缓存是否过期
  • 4判断缓存是否过期
    • 4.1缓存未过期,返回商铺信息
    • 4.2缓存过期,尝试获取互斥锁
  • 5获取互斥锁
  • 6判断互斥锁是否成功
    • 6.1获取锁失败,返回商铺信息
    • 6.2获取锁成功,再次判断缓存是否过期(这里是避免在获取锁的时间内,缓存已经被重建)
    • 6.3缓存未过期,返回商铺信息
    • 6.4缓存过期,缓存重建
  • 7.开启缓存重建
    • 7.1开启独立线程,根据id查询数据库
    • 7.2将商铺数据写入Redis建立缓存,并设置逻辑过期时间
  • 8释放互斥锁
  • 9返回过期的商铺信息

首先要进行数据预热,就是先将热点数据缓存到Redis缓存中。这里建立的缓存就已经是有逻辑时间的缓存

  • 1.查询店铺信息
  • 2.封装逻辑过期时间
  • 3.写入Redis
    /**
     * 提前将数据存入Redis(也叫缓存预热)
     * @param id
     */
    public void saveShop2Redis(Long id,Long expireSeconds) {
        Shop shop = query().eq("id", id).one();
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_XIANG+id,JSONUtil.toJsonStr(redisData));

    }
@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

    /**
     * 逻辑过期解决缓存击穿问题
     *
     * @param id
     * @return
     */
    public Shop queryWithShopLogicalExpiration(Long id) {
        //1.获取商铺id
        //2.从Redis中查询商铺缓存
        String shopJSON = stringRedisTemplate.opsForValue().get(CACHE_SHOP_XIANG + id);
        //3.判断缓存是否存在
        if (StrUtil.isBlank(shopJSON)) {
            //3.1缓存不存在,返回空
            return null;
        }

        //3.2缓存存在,需要判断缓存是否过期
        RedisData redisData = JSONUtil.toBean(shopJSON, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        JSONObject shopJSONObject = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(shopJSONObject, Shop.class);
        //4判断缓存是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //4.1缓存未过期,返回商铺信息
            return shop;
        }

        //4.2缓存过期,尝试获取互斥锁
        //5获取互斥锁
        boolean isLock = tryLock(LOCK_SHOP_KEY + id);
        //6判断互斥锁是否成功
        if (!isLock) {
            //6.1获取锁失败,返回商铺信息

            return shop;
        }
        //6.2获取锁成功,再次判断缓存是否过期(这里是避免在获取锁的时间内,缓存已经被重建)
        if (expireTime.isAfter(LocalDateTime.now())) {
            //6.3缓存未过期,返回商铺信息
            return shop;
        }
        //6.4缓存过期,缓存重建
        // 7.开启缓存重建
        //7.1开启独立线程,根据id查询数据库
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                //7.2将商铺数据写入Redis建立缓存,并设置逻辑过期时间

                this.saveShop2Redis(id, 20L);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                //8释放互斥锁

                unlock(LOCK_SHOP_KEY + id);
            }
        });
        //9返回商铺信息
        return shop;
    }

优惠卷秒杀

全局唯一ID

        全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足以下特性

  1. 唯一性
  2. 高可用
  3. 递增行
  4. 安全性
  5. 高性能

        ID组成部分:

符号位:1bit,永远为0

时间戳:31bit,以秒为单位,可以使用69年

序列号:32bit,秒内的计数器,可以支持2^32个不同ID


@Component
public class RedisIdWorker {
    /**
     * 开始时间戳
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /**
     * 序列号的位数
     */
    private static final int COUNT_BITS = 32;

    private StringRedisTemplate stringRedisTemplate;

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

    public long nextId(String keyPrefix) {
        // 1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        // 2.生成序列号
        // 2.1.获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2.自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        // 3.拼接并返回
        return timestamp << COUNT_BITS | count;
    }
}
  • 全局唯一ID生成策略
    • UUID
    • Redis自增
    • 雪花(snowflake)算法
    • 数据库自增
  • Redis自增ID策略
    • 每天一个key,方便统计订单量
    • ID构造是时间戳+计数器

实现秒杀下单

  • 下单时需要判断两点:
    • 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
    • 库存是否充足,不足则无法下单

  • 1.获取优惠卷id
  • 2.查询优惠卷信息
  • 3.判断秒杀是否开始
  • 4.判断秒杀是否结束
  • 5.判断库存是否充足
  • 6.扣减库存
  • 7.创建订单
    • 7.1获取订单id
    • 7.2获取用户id
    • 7.3获取代金卷id
  • 8.返回订单
    @Override
    @Transactional
    public Result seckillVoucherXiang(Long voucherId) {
        //1.获取优惠卷id
        //2.查询优惠卷信息
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);

        //3.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀活动尚未开始");
        }
        //4.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀活动已经结束");
        }
        //5.判断库存是否充足
        if (voucher.getStock() < 1) {
            return Result.fail("库存不足");

        }
        //6.扣减库存
        boolean istrue = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).update();
       if (!istrue){
           return Result.fail("库存不足");
       }
        //7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //7.1获取订单id
        long orederID = redisIdWorker.nextId("voucherId");
        voucherOrder.setId(orederID);
        //7.2获取用户id
        Long userID = UserHolder.getUser().getId();
        voucherOrder.setUserId(userID);
        //7.3获取代金卷id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //8.返回订单
        return Result.ok(orederID);
    }

关于超卖

        超卖问题就是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:

        悲观锁:

        认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行

  •         例如Synchronized、Lock都属于悲观锁

        乐观锁:

        认为线程安全不一定会发生,因此不加锁,只是在更新数据时取判断有没有其他线程对数据做了修改

  •         如果没有修改则认为时安全的,自己才更新数据
  •         如果已经被其他线程修改说明发生了安全问题,此时可以重试或异常

关于超卖我们可以加一个乐观锁,乐观锁的关键是判断之前查询得到的数据有被修改过,常见的方式有两种:

  • 1.版本号法:顾名思义,就是在修改数据时加入一个version,如果修改的时候version与自己得到的version 不相同,那么就修改失败,可以尝试重试或异常
  • 2.CAS法:就是更改数据,或者删除库存的时候,判断库存是否大于0,如果大于零,则扣除成功
  •         //6.扣减库存
            boolean istrue = seckillVoucherService
                    .update()
                    .setSql("stock=stock-1")
                    .eq("voucher_id", voucherId)
                    .gt("stock",0)
                    .update();
           if (!istrue){
               return Result.fail("库存不足");
           }

超卖这样的线程安全问题,解决方案有哪些

1.悲观锁:添加同步锁,让线程串行执行

  •         优点:简单粗暴
  •         缺点:性能一般

2.乐观锁:不加锁,在更新时判断是否有其他线程在修改

  •         优点:性能好
  •         缺点:存在成功率低的问题 

一人一单

        秒杀业务,一人只能成功一单

  • 1.获取优惠卷id
  • 2.查询优惠卷信息
  • 3.判断秒杀是否开始
    • 3.1没有开始,返回异常结果
  • 4.判断秒杀是否结束
    • 4.1已经结束,返回异常结果
  • 5.判断库存是否充足
    • 5.1不足,返回异常结果
  • 6.根据优惠卷和用户查询订单
  • 7.判断订单是否存在
    • 7.1存在,返回异常结果
  • 8.不存在.扣减库存
  • 9.创建订单
    • 9.1获取订单id
    • 9.2获取用户id
    • 9.3获取代金卷id
  • 10.将订单储存在数据库中
  • 11.返回订单

    @Override
    @Transactional
    public Result seckillVoucherXiang(Long voucherId) {
        //1.获取优惠卷id
        //2.查询优惠卷信息
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        //3.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
            //3.1没有开始,返回异常结果
            return Result.fail("活动尚未开始");
        }
        //4.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())){
            //4.1已经结束,返回异常结果
            return Result.fail("活动已经结束");
        }

        //5.判断库存是否充足
        if (voucher.getStock()<1){
            //5.1不足,返回异常结果
            return Result.fail("库存不足");
        }
        UserDTO user = UserHolder.getUser();
        Long userID = user.getId();
        //6.根据优惠卷和用户查询订单
        int count = query().eq("user_id", userID).eq("voucher_id", voucherId).count();
        //7.判断订单是否存在
        if (count>0){
            //7.1存在,返回异常结果
            return Result.fail("订单已存在,请勿重复下单");

        }
        //8.不存在.扣减库存
        boolean update = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).gt("stock", 0).update();
        if (!update){
            return Result.fail("库存不足");

        }
        //9.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //9.1获取订单id
        long voucherId1 = redisIdWorker.nextId("voucherId");
        voucherOrder.setId(voucherId1) ;
        //9.2获取用户id
        voucherOrder.setUserId(userID);

        //9.3获取代金卷id
        voucherOrder.setVoucherId(voucherId);
        //10.将订单储存在数据库中
        save(voucherOrder);
        //11.返回订单
        return Result.ok(voucherId1);
}

        以上操作,存在线程安全问题,比如多线程下,会在判断订单是否存在的同时,多个线程同时进行,造成一个用户下了不止一单  

        这时候就要用悲观锁来完成。让获取订单的时候只有一个线程进行,就是在获取UserID的时候加上synchronized。

    @Override
    public Result seckillVoucherXiang(Long voucherId) {
        //1.获取优惠卷id
        //2.查询优惠卷信息
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        //3.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            //3.1没有开始,返回异常结果
            return Result.fail("活动尚未开始");
        }
        //4.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            //4.1已经结束,返回异常结果
            return Result.fail("活动已经结束");
        }

        //5.判断库存是否充足
        if (voucher.getStock() < 1) {
            //5.1不足,返回异常结果
            return Result.fail("库存不足");
        }
        UserDTO user = UserHolder.getUser();
        Long userID = user.getId();
        synchronized (userID.toString().intern()) {
            return createVoucherOrder(voucherId);
        }
    }
    


    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        UserDTO user = UserHolder.getUser();
        Long userID = user.getId();
        //6.根据优惠卷和用户查询订单
        int count = query().eq("user_id", userID).eq("voucher_id", voucherId).count();
        //7.判断订单是否存在
        if (count > 0) {
            //7.1存在,返回异常结果
            return Result.fail("订单已存在,请勿重复下单");

        }
        //8.不存在.扣减库存
        boolean update = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).gt("stock", 0).update();
        if (!update) {
            return Result.fail("库存不足");

        }
        //9.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //9.1获取订单id
        long voucherId1 = redisIdWorker.nextId("voucherId");
        voucherOrder.setId(voucherId1);
        //9.2获取用户id
        voucherOrder.setUserId(userID);

        //9.3获取代金卷id
        voucherOrder.setVoucherId(voucherId);
        //10.将订单储存在数据库中
        save(voucherOrder);
        //10.返回订单
        return Result.ok(voucherId1);
    }

        以上的做法,在逻辑上是没有问题的,但是忽略了@Transactional事务在Service里面方法内部调用会失效的问题Spring事务失效的场景。我这里采用的是通过AopContent类来解决问题。

        采用AopContent类解决问题要先完成两步

  • 1.导入aspectJ依赖
  •         <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
            </dependency>
  • 2.启动暴露代理对象
  • @EnableAspectJAutoProxy(exposeProxy = true)
    @MapperScan("com.hmdp.mapper")
    @SpringBootApplication
    public class HmDianPingApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(HmDianPingApplication.class, args);
        }
    
    }
    @Override
    public Result seckillVoucherXiang(Long voucherId) {

        //1.获取优惠卷id
        //2.查询优惠卷信息
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        //3.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            //3.1没有开始,返回异常结果
            return Result.fail("活动尚未开始");
        }
        //4.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            //4.1已经结束,返回异常结果
            return Result.fail("活动已经结束");
        }

        //5.判断库存是否充足
        if (voucher.getStock() < 1) {
            //5.1不足,返回异常结果
            return Result.fail("库存不足");
        }
        UserDTO user = UserHolder.getUser();
        Long userID = user.getId();
        synchronized (userID.toString().intern()) {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }

    @Override
    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        UserDTO user = UserHolder.getUser();
        Long userID = user.getId();
        //6.根据优惠卷和用户查询订单
        int count = query().eq("user_id", userID).eq("voucher_id", voucherId).count();
        //7.判断订单是否存在
        if (count > 0) {
            //7.1存在,返回异常结果
            return Result.fail("订单已存在,请勿重复下单");

        }
        //8.不存在.扣减库存
        boolean update = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).gt("stock", 0).update();
        if (!update) {
            return Result.fail("库存不足");

        }
        //9.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //9.1获取订单id
        long voucherId1 = redisIdWorker.nextId("voucherId");
        voucherOrder.setId(voucherId1);
        //9.2获取用户id
        voucherOrder.setUserId(userID);

        //9.3获取代金卷id
        voucherOrder.setVoucherId(voucherId);
        //10.将订单储存在数据库中
        save(voucherOrder);
        //10.返回订单
        return Result.ok(voucherId1);
    }

分布式锁

        以上的一人一单是在单机模式下可以完成,但是在多机模式就会发生错误,原因是新的一个会有新的JVM,会有不同的锁监视器来监视锁。采用分布式锁可以解决这种问题。

        分布式锁:满足分布式系统或集群模式下多进程可见并互斥的锁 

有以下几个基本特点

  • 多进程可见
  • 高可用
  • 安全性
  • 互斥
  • 高性能

分布式锁的实现

基于Redis实现分布式锁

        原来加锁是:

         UserDTO user = UserHolder.getUser();
         Long userID = user.getId();
         synchronized (userID.toString().intern()) {
         IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
         return proxy.createVoucherOrder(voucherId);
         }

        现在加锁:

public interface ILockXiang {
    boolean tryLock(Long timestamp);

    void unlock();
}
public class ILockXiangImpl implements ILockXiang{
    private StringRedisTemplate stringRedisTemplate;
    private Long name;

    public ILockXiangImpl(StringRedisTemplate stringRedisTemplate, Long name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(Long timestamp) {
        String threadName = Thread.currentThread().getName();
        Boolean istrue = stringRedisTemplate.opsForValue().setIfAbsent("lock" + name, threadName, timestamp, TimeUnit.SECONDS);

        return BooleanUtil.isTrue(istrue);
//        return Boolean.TRUE.equals(istrue);
    }

    @Override
    public void unlock() {
             stringRedisTemplate.delete("lock" + name);
    }
}
        UserDTO user = UserHolder.getUser();
        Long userID = user.getId();
        ILockXiangImpl iLockXiang = new ILockXiangImpl(stringRedisTemplate, userID);
        boolean tryLock = iLockXiang.tryLock(1200L);
        if (!tryLock) {
            return Result.fail("您已经下过单了,请到下单界面查看详情");
        }

        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            iLockXiang.unlock();
        }

         这样在两个客户端同时发请求,也会锁的住.但是存在一个问题,就是锁误删的情况。比如A处理业务,加锁但是业务时间超过了加锁时间,锁超时会自动释放,A业务并不知道仍旧在处理A业务,这时B业务过来,因为上一个锁已经被释放,所以B业务同样可以获得锁,如果A业务在B业务处理前释放锁的话,这里A业务释放的锁就是B业务的锁。

        所以释放锁的时候要进行判断,这个锁是不是自己的锁。在尝试获取锁已经释放锁的地方加入判断满足:

  •         1.在获取锁时存入线程标识(可以使用UUID表示)
  •         2.在释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致
    •       如果一致则释放锁
    •       如果不一致则不释放锁
public class ILockXiangImpl implements ILockXiang {
    private StringRedisTemplate stringRedisTemplate;
    private Long name;
    private static final String LOCK_PREFIX = UUID.randomUUID().toString(true);

    public ILockXiangImpl(StringRedisTemplate stringRedisTemplate, Long name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(Long timestamp) {
        String threadName = LOCK_PREFIX + Thread.currentThread().getName();
        Boolean istrue = stringRedisTemplate.opsForValue().setIfAbsent("lock" + name, threadName, timestamp, TimeUnit.SECONDS);

        return BooleanUtil.isTrue(istrue);
//        return Boolean.TRUE.equals(istrue);
    }

    @Override
    public void unlock() {
        String threadName = LOCK_PREFIX + Thread.currentThread().getName();
        String id = stringRedisTemplate.opsForValue().get("lock" + name);
        if (id.equals(threadName)){
            stringRedisTemplate.delete("lock" + name);
        }

    }
}

基于Redis分布式锁优化

        基于setnx实现的分布式锁存在以下问题:

  • 1.不可重入
    • 同一线程无法多次获取同一把锁
  • 2.不可重试
    •  获取锁只尝试一次就返回false,没有重试机制
  • 3.超时释放
    • 锁超时释放虽然可以避免死锁,但如果业务执行耗时较长,也会导致锁释放,存在安全隐患
  • 4.主从一致性
    • 如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现。

Redisson

        Redisson是一个在Redis的基础上实现的Java驻内存数据网络。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

        官方地址https://redisson.org        GitHub地址https://github.com/redisson/redisson

         实现Redisson

  • 1.导入依赖
    •         <!--redisson-->
              <dependency>
                  <groupId>org.redisson</groupId>
                  <artifactId>redisson</artifactId>
                  <version>3.13.6</version>
              </dependency>

  • 2.配置Redisson客户端
    • @Configuration
      public class RedissonConfig {
      
          @Bean
          public RedissonClient redissonClient(){
              // 配置
              Config config = new Config();
              config.useSingleServer().setAddress("redis://192.168.80.135:6379");
              // 创建RedissonClient对象
              return Redisson.create(config);
          }
      }
  • 3.引入Redisson
    •     @Resource
          private RedissonClient redissonClient;

使用Redisson锁后如何加锁

        UserDTO user = UserHolder.getUser();
        Long userID = user.getId();
        RLock lock = redissonClient.getLock(LOCK_SHOP_KEY + userID);
        boolean tryLock = lock.tryLock();
        if (!tryLock) {
            return Result.fail("您已经下过单了,请到下单界面查看详情");
        }

        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            lock.unlock();
        }

Redisson可重入锁原理

秒杀优化 

        原本为以下构造,串行化一条龙进行,但是减库存以及创建订单的操作是针对于数据库,而且是写入操作,效率会比较低的,如果一条龙的进行,一次的时间过于漫长

        这是优化后的样子,将其拆分为两个部分,一个复杂前面的,一个负责后面的数据库的操作,这样,前面的进行完了就可以直接返回 

 

 改进秒杀业务,提高并发性能

  • 1.新增秒杀优惠卷的同时,将优惠卷信息保存到Redis中
  • 2.基于LUA脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
  • 3.如果抢购成功,将优惠卷id和用户id封装后存入阻塞队列
  • 4.开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能

1.新增秒杀优惠卷的同时,将优惠卷信息保存到Redis中

    @Override
    @Transactional
    public void addSeckillVoucher(Voucher voucher) {
        // 保存优惠券
        save(voucher);
        // 保存秒杀信息
        SeckillVoucher seckillVoucher = new SeckillVoucher();
        seckillVoucher.setVoucherId(voucher.getId());
        seckillVoucher.setStock(voucher.getStock());
        seckillVoucher.setBeginTime(voucher.getBeginTime());
        seckillVoucher.setEndTime(voucher.getEndTime());
        seckillVoucherService.save(seckillVoucher);
        // 保存秒杀库存到Redis中
        stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
    }
}

2.基于LUA脚本,判断秒杀库存、一人一单,决定用户是否抢购成功

2.1执行lua脚本

    private static final DefaultRedisScript<Long> SECKILXAINGL_SCRIPT;

    static {
        SECKILXAINGL_SCRIPT = new DefaultRedisScript<>();
        SECKILXAINGL_SCRIPT.setLocation(new ClassPathResource("seckillxiang.lua"));
        SECKILXAINGL_SCRIPT.setResultType(Long.class);
    }
    @Override
    public Result seckillVoucherXiang(Long voucherId) {
        Long userId = UserHolder.getUser().getId();

        /**
         execute需要传三个参数
         1.lua脚本
         2.KEYS[]    如果为null不可以直接传null,要传一个空的List串Collections.emptyList(),
         3.ARGV[]
         */
        Long lua = stringRedisTemplate.execute(
                SECKILXAINGL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString()
        );
        int i = lua.intValue();
        if (i!=0){
            if (i==1){
                return Result.fail("库存不足");
            }
            else {
                return Result.fail("请不要重复下单");
            }
        }
        return Result.ok(0);

    }

2.2lua脚本

  • 1.我按部就班编写lua脚本
    • --1.判断库存是否充足
      --1.1获取优惠卷id
      local voucherId=ARGV[1]
      --1.2查询优惠卷id的库存是否充足
      --redis.call("get",voucherId)
      --1.3获取库存key
      local stockKey="seckillxiang:stock" ..voucherId
      --1.4库存充足,判断用户是否下单
      if(tonumber(redis.call("get",stockKey))<=0) then
      --1.5库存不足返回1
          return 1
      end
      
      --2.判断用户是否下单
      --2.1获取用户id
      local userID=ARGV[2]
      --2.2获取订单Key
      local orderKey="seckillxiang:order" .. userID
      --2.2根据用户id查询是否在set集合中存在
      --redis.call("sismember",orderKey,userID)
      
      --2.3用户已下单返回2
      if(redis.call("sismember",orderKey,userID)==1) then
          return 2
      end
      --3.扣减库存
      redis.call("incrby",stockKey,-1)
      --3.1将userID存入当前优惠卷的Set集合
      redis.call("sadd",orderKey,userID)
      --3.2返回0
      return 0

  • 2.比较规范的编写脚本
    • -- 1.参数列表
      -- 1.1.优惠券id
      local voucherId = ARGV[1]
      -- 1.2.用户id
      local userId = ARGV[2]
      -- 1.3.订单id
      local orderId = ARGV[3]
      
      -- 2.数据key
      -- 2.1.库存key
      local stockKey = 'seckill:stock:' .. voucherId
      -- 2.2.订单key
      local orderKey = 'seckill:order:' .. voucherId
      
      -- 3.脚本业务
      -- 3.1.判断库存是否充足 get stockKey
      if(tonumber(redis.call('get', stockKey)) <= 0) then
          -- 3.2.库存不足,返回1
          return 1
      end
      -- 3.2.判断用户是否下单 SISMEMBER orderKey userId
      if(redis.call('sismember', orderKey, userId) == 1) then
          -- 3.3.存在,说明是重复下单,返回2
          return 2
      end
      -- 3.4.扣库存 incrby stockKey -1
      redis.call('incrby', stockKey, -1)
      -- 3.5.下单(保存用户)sadd orderKey userId
      redis.call('sadd', orderKey, userId)
      -- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
      redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
      return 0

3.如果抢购成功,将优惠卷id和用户id封装后存入阻塞队列

4.开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能

 

stream消息队列 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值