您现在的位置是:首页 > 正文

[学习记录] Redis 5. 事务和锁机制,秒杀案例 Demo

2024-02-01 01:17:07阅读 2

Redis 5. 事务和锁机制,秒杀案例 Demo

参考课程:https://www.bilibili.com/video/BV1Rv41177Af

参考书:https://blog.csdn.net/liu8490631/article/details/124290851

1. Redis 事务定义

Redis 事务是一个单独的隔离操作,事务中的所有命令都会序列化,按顺序地执行。

事务在执行过程中,不会被其他地客户端发送来地命令请求所打断。

Redis 事务地主要作用就是串联多个命令防止别地命令插队。

2. Multi,Exec,Discard

输入 Multi 后,输入的命令会依次进入命令队列,但不会执行,直到输入 Exec 后,Redis 会将之前的命令队列中的命令依次执行,discard 会抛弃命令队列中的命令。

类似于 MySQL 中 start transaction/begin 开启事务,commit 提交事务,rollback 回滚事务。

3. 事务错误处理

当 Redis 事务中有命令报错 ERROR,则该事务中的所有命令都不执行:
在这里插入图片描述

事务中没有错误,事务执行过程中出现错误,则报错的语句不执行,其他命令正常执行:
在这里插入图片描述

4. 事务冲突的问题

一般情况下,三个事务同时修改余额,可能会导致余额不够买一个商品但是买下来了的情况。这就是事务冲突。解决冲突的方法:

4.1 悲观锁(Pessimistic Lock)

每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞,直到它拿到锁。

  • 传统的关系型数据库里面就用到了很多这种锁机制,比如行锁表锁读锁写锁等,都是在做操作之前先上锁。

  • 缺点:效率低

4.2 乐观锁(Optimistic Lock)

每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。

  • 乐观锁适用于多读的应用类型,这样可以提高吞吐量

  • Redis 采用的就是这种 check-and-set 机制实现事务的

  • 典型场景:抢票

4.3 WATCH key [key …]

在执行 MULTI 之前,先执行 watch key1 [key2 ...],可以监视一个(或者多个)key,如果在事务执行之前这个或这些 key 被其他命令所改动,那么事务将被打断

事务 1 和事务 2 同时 watch 一个 key,然后都 multi 开启事务:

  1. 事务 1 对 key 进行修改,exec,修改成功
  2. 事务 2 对 key 进行修改,exec,可以看到返回(nil),修改失败
  • unwatch 可以取消 key 的监视,若执行 watch 命令后又执行了 exec 或者 discard 的话,就不用执行 unwatch 了。

5. Redis 事务三特性

5.1 单独隔离性

  • 事务中的所有命令都会序列化,按照顺序执行。
  • 事务在执行过程中,不会被其他的客户端发送来的命令请求所打断

5.2 没有隔离级别的概念

  • 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。

5.3 不保证原子性

  • 事务执行过程中如果有一条命令执行失败,其后面的命令仍然会被执行,没有回滚
  • 若在队列中发生语法错误等则该事务中的所有元素都会被 discard

6. 秒杀案例 Demo

需求:

  • 商品库存:个数减少
  • 秒杀成功清单:加人

6.0 前情提要

SpringBoot 默认序列化方式可读性太差了:
在这里插入图片描述
换成 Jackson

@Configuration
public class RedisConfig {
    /**
     * 设置redis键值的序列化方式
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        /*
         * value值的序列化可采用如下几种方式
         * 1.默认采用JdkSerializationRedisSerializer 序列化后的长度最短,时间适中,但不是明文显示
         * 2.采用Jackson2JsonRedisSerializer 明文显示,序列化速度最快,长度适中,但会使存入redis中timestamp类型的数据以long类型存储
         * 3.采用GenericJackson2JsonRedisSerializer 明文显示,在redis中显示了@class字段保存有类型的包路径,反序列化更容易,但是序列化时间最长,长度最大,明文显示
         * 4.自定义FastJsonRedisSerializer实现RedisSerializer接口
         *
         * 这里使用Jackson2JsonRedisSerializer,并对日期类型做特别处理
         */
        Jackson2JsonRedisSerializer<Object> redisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        /*
         *  指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
         *  过期方法:om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
         */
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.WRAPPER_ARRAY);

        // 日期序列化处理
        om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        om.registerModule(new Jdk8Module())
                .registerModule(new JavaTimeModule())
                .registerModule(new ParameterNamesModule());
        redisSerializer.setObjectMapper(om);

        // 设置值(value)的序列化采用Jackson2JsonRedisSerializer
        template.setValueSerializer(redisSerializer);
        template.setHashValueSerializer(redisSerializer);

        // 设置键(key)的序列化采用StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());

        template.afterPropertiesSet();

        return template;
    }
}

字符串的 value 保存后是 "\"world\"" 的形式,数字类型默认为 Integer,即使字符串进去出来也是 Integer,小数默认就是 Double。

在这里插入图片描述

6.1 简单案例

package com.xz.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.*;

import java.util.Set;

@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
    @Autowired
    private RedisTemplate redisTemplate;
    
    @PostMapping("/test")
    public boolean test2 (String uid, String prodid) {
        ValueOperations opsString = redisTemplate.opsForValue();
        SetOperations opsSet = redisTemplate.opsForSet();

        // 判空
        if (uid == null || prodid == null) {
            return false;
        }

        // 拼接 key, 库存 key , 秒杀成功用户 key
        String kcKey = "sk:" + prodid + ":qt";
        String userKey = "sk:" + prodid + ":user";

        // 获取库存, 若库存为 null, 秒杀未开始
        Object kc = opsString.get(kcKey);

        if (kc == null) {
            System.out.println("秒杀活动未开始!");
            return false;
        }

        // 判断是否重复秒杀操作
        if (Boolean.TRUE.equals(opsSet.isMember(userKey, uid))) {
            System.out.println("已经参与秒杀, 不能重复参与!");
            return false;
        }

        // 判断如果商品数量 < 1, 秒杀结束
        if ((Integer)kc < 1) {
            System.out.println("秒杀已经结束!");
            return false;
        }

        // 秒杀过程  库存 -1, 秒杀成功的用户添加进清单
        opsString.decrement(kcKey);
        opsSet.add(userKey, uid);
        System.out.println("用户 " + uid + " 参与 " + prodid + " 的秒杀活动参与成功!");
        return true;
    }

6.3 JMeter 测试

在这里插入图片描述

在这里插入图片描述

Redis 设置 0101 号商品库存为 10:

SET sk:0101:qt 10

500 个线程,每个线程抢两次。

在这里插入图片描述
10 件商品卖出去了 200 件。
严重超卖!

6.4 超卖问题

下面这代码并没有完全解决超卖,还不知道啥原因,记录一下,往后学一学,能解决了再来改!我设置一千个线程都超了!

找到原因了找到原因了,监视库存应该放在所有对 Redis 访问或者修改操作之前!

错误代码,监视库存只放在 Redis 写操作之前:

@PostMapping("/test")
    public boolean test2 (String uid, String prodid) {
        // 判空
        if (uid == null || prodid == null) {
            return false;
        }

        // 拼接 key, 库存 key , 秒杀成功用户 key
        String kcKey = "sk:" + prodid + ":qt";
        String userKey = "sk:" + prodid + ":user";


        // 获取库存, 若库存为 null, 秒杀未开始
        Object kc = redisTemplate.opsForValue().get(kcKey);

        if (kc == null) {
            System.out.println("秒杀活动未开始!");
            return false;
        }

        // 判断是否重复秒杀操作
        if (Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(userKey, uid))) {
            System.out.println("已经参与秒杀, 不能重复参与!");
            return false;
        }

        // 判断如果商品数量 < 1, 秒杀结束
        if ((Integer)kc < 1) {
            System.out.println("秒杀已经结束!");
            return false;
        }

        /*
        * 事务
        * */

        SessionCallback sessionCallback = new SessionCallback() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                // 监视库存
                operations.watch(kcKey);
                operations.multi();
                // 秒杀过程  库存 -1, 秒杀成功的用户添加进清单
                redisTemplate.opsForValue().decrement(kcKey);
                redisTemplate.opsForSet().add(userKey, uid);
                return operations.exec();
            }
        };

        List result = (List) redisTemplate.execute(sessionCallback);

        if (result == null || result.size() == 0) {
            System.out.println("秒杀失败了......");
            return false;
        }

        System.out.println("用户 " + uid + " 参与 " + prodid + " 的秒杀活动参与成功!");
        return true;
    }

在这里插入图片描述

正确代码,监视库存应该放在所有对 Redis 访问或者修改操作之前:

@PostMapping("/test")
    public boolean test2 (String uid, String prodid) {
        // 判空
        if (uid == null || prodid == null) {
            return false;
        }

        // 拼接 key, 库存 key , 秒杀成功用户 key
        String kcKey = "sk:" + prodid + ":qt";
        String userKey = "sk:" + prodid + ":user";


        /*
        * 事务
        * */
        SessionCallback sessionCallback = new SessionCallback() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                // 监视库存
                List<String> list = new ArrayList<>();
                list.add(kcKey);
                operations.watch(list);
                Object kc = redisTemplate.opsForValue().get(kcKey);

                // 获取库存, 若库存为 null, 秒杀未开始
                if (kc == null) {
                    System.out.println("秒杀活动未开始!");
                    return null;
                }

                // 判断是否重复秒杀操作
                if (Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(userKey, uid))) {
                    System.out.println("已经参与秒杀, 不能重复参与!");
                    return null;
                }

                // 判断如果商品数量 < 1, 秒杀结束, (再次获取)
                if ((Integer) kc < 1) {
                    System.out.println("秒杀已经结束!");
                    return null;
                }

                operations.multi();
                // 秒杀过程  库存 -1, 秒杀成功的用户添加进清单
                redisTemplate.opsForValue().decrement(kcKey);
                redisTemplate.opsForSet().add(userKey, uid);
                return operations.exec();
            }
        };

        List result = (List) redisTemplate.execute(sessionCallback);

        if (result == null || result.size() == 0) {
            System.out.println("秒杀失败了......");
            return false;
        }

        System.out.println("用户 " + uid + " 参与 " + prodid + " 的秒杀活动参与成功!");
        return true;
    }

再次测试:
在这里插入图片描述

完美!

6.5 连接超时问题

Q:Redis 不是单线程吗?为什么还需要连接池?

A: https://blog.csdn.net/forBurnInG/article/details/103893680

Q:连接池最大连接数越大越好?

A:https://zhuanlan.zhihu.com/p/396034724

因为我用的 SpringDataRedis,RedisTemplate 默认用的就是连接池,所以不会遇到连接超时问题。

拿 Mysql 举个例子:

不用连接池:
在这里插入图片描述

使用连接池:

在这里插入图片描述

不适用连接池的步骤:

  1. 建立 TCP 连接
  2. 建立数据库连接
  3. 执行语句
  4. 断开数据库连接
  5. 断开 TCP 连接

连接池的作用:

  • 资源重用,节省每次连接服务带来的消耗
  • 更快的响应速度,直接利用了现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而缩减了系统整体响应时间。

6.6 (使用乐观锁)库存遗留问题

设置商品大一点,然后测试:

在这里插入图片描述

出现了库存遗留问题。

Lua 脚本解决该问题,我再学学,看看有没有其他方法,有的话我再来补充!
https://www.jianshu.com/p/a555facfd6c8

网站文章

  • P5735 【深基7.例1】距离函数

    P5735 【深基7.例1】距离函数给出平面坐标上不在一条直线上三个点坐标 (x1,y1),(x2,y2),(x3,y3)(x_1,y_1),(x_2,y_2),(x_3,y_3)(x1,y1),(x...

    2024-02-01 01:16:37
  • C# - JSON详解

    C# - JSON详解

    最近在做微信开发时用到了一些json的问题,就是把微信返回回来的一些json数据做一些处理,但是之前json掌握的不好,浪费了好多时间在查找一些json有关的转换问题,我所知道的方法只有把json序列化和反序列化一下,但是太麻烦了我觉得,所以就在找一些更简单又方便使用的方法。也许这个会有用吧,所以先放到这以后能用到的。原文出处:http://www.cnblogs.com/mcgra...

    2024-02-01 01:16:29
  • yum安装mongodb报错

    yum安装mongodb报错

    今天在尝试yum安装mongodb时,发现了问题:这是因为你以前用的是CENTOS现在是redhat 红帽的yum安装软件的时候要验证的看是不是红帽的软件,是红帽的软件可以安装不是就失败.因此,我们需要将gpgcheck=1改成gpgcheck=0即可。gpgcheck=1表示需要验证,0表示不需要验证。成功!...

    2024-02-01 01:16:13
  • 用nginx反向代理Jenkins遇到的testForReverseProxySetup问题

    用nginx反向代理Jenkins遇到的testForReverseProxySetup问题

    又一次开始了Jenkins征程,其实以前我就遇到了这个问题,如图 你说你报这个错误鬼知道是为什么,当然了,我们也不能太苛求,Jenkins怎么可能知道具体是什么问题呢?算啦,我们自己去看Jenkins的日志吧,我发现 WARNING: http://jenkins.tangxuyang.cn/manage vs. http: 然后又结合chrome的F12,如下 就是这个请求没有正...

    2024-02-01 01:15:44
  • 为什么在vue3中每个页面都需要引用ref,reactive的问题

    为什么在vue3中每个页面都需要引用ref,reactive的问题

    在 Vue 3 中,对响应式数据的追踪和更新机制进行了优化,使得响应式数据的更新更加高效。,而不是自动引入,是因为 Vue 3 中引入了 Tree Shaking 机制,这种机制可以对无用的代码进行剪...

    2024-02-01 01:15:37
  • 使用 kind 1 分钟启动一个本地 k8s 开发集群

    使用 kind 1 分钟启动一个本地 k8s 开发集群

    使用 kind 1 分钟启动一个本地 k8s 开发集群kind 简介Github 地址:https://github.com/kubernetes-sigs/kindkind 是一个快速启动 kube...

    2024-02-01 01:15:33
  • 【python】时间处理函数以及文件操作

    1. 时间函数模块(库)-使用流程:先导入,再引用1:导入 方式一:import 模块名 引用:模块名.函数名() 方式二:from 模块名 import 函数名/变量/类 ...

    2024-02-01 01:15:05
  • 宝塔部署来客电商时,出现“open_basedir restriction in effect”错误解决方案 出现如下错误:

    宝塔部署来客电商时,出现“open_basedir restriction in effect”错误解决方案 出现如下错误:

    宝塔部署来客电商时,出现“open_basedir restriction in effect”错误解决方案Application/LKT/webapp/_compile

    2024-02-01 01:14:59
  • 国外黑客站点收集

    著名的黑客站点 国外黑客安全 http://www.deadly.org/ 大量关于OpenBSD的资料文档教程 国外黑客安全 http://www.guninski.com/ 安全专家Guninsk...

    2024-02-01 01:14:51
  • C++ 动态开辟二维数组的的方法

    近日写到一个程序,用到了要动态开辟二维数组,一想,自己就会两种。一者:用new在堆上开辟;二者:用vector开辟。技巧没有多少,但是确实是折腾了我半天!首先,大家去网上搜一下,动态开辟二维数组的文章特别多,再加上我这篇就更多了,我本不想写这篇博文的。但看了网上各位“大虾”“大牛”写的,觉得还是有必要写一下!给各位讲清楚点,以防被网上质量残次不齐的文章误导了。 写...

    2024-02-01 01:14:44