IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> 3.Redis事务&&秒杀案例 -> 正文阅读

[大数据]3.Redis事务&&秒杀案例

1. Redis事务

1.1 Redis的事务定义

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

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

三个阶段:

  • 事务开始
  • 命令入队
  • 事务执行

1.2 Multi、Exec、discard

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

组队的过程中可以通过discard来放弃组队

在这里插入图片描述

案例:

组队成功,提交成功

在这里插入图片描述

组队阶段报错,提交失败

在这里插入图片描述

组队成功,提交有失败的情况

在这里插入图片描述

1.3 事务的错误处理

组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。

在这里插入图片描述

如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

在这里插入图片描述

1.4 WATCH

WATCH命令是一个乐观锁,它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被更改过,如果是的化,服务器拒绝执行事务,并向客户端返回代表事务执行失败的空回复。

1.4.1 悲观锁和乐观锁

  • 悲观锁

    当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。

    悲观锁,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。

    悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)

    在这里插入图片描述

  • 乐观锁

    乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。

    乐观锁采取了更加宽松的加锁机制。也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。乐观锁的实现:

    1. CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。
    2. 版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会 +1。当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

    在这里插入图片描述

1.4.2 具体过程

客户端A开始watch name,并开启事务

在这里插入图片描述

客户端B更改name的值

在这里插入图片描述

发现客户端A的事务执行失败

注意点:在一个客户端中watch name,在本客户端开启事务执行更改name的值,watch会失效。必须在多个客户端中更改才会生效。

1.4.3 unwatch

取消 WATCH 命令对所有 key 的监视。

如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。

1.5 Redis事务三特性

  • 单独的隔离操作

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

  • 没有隔离级别的概念

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

  • 不保证原子性

    事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

2. Redis事务—秒杀实例

2.1 单机模拟

在商品秒杀的场景中,我们需要两个映射来发反应秒杀的状况

mapper1:商品id->库存个数

mapper2:商品id->抢到者id的List

秒杀开始后,每当有一个人抢到商品,mapper1中商品的库存数量-1
mapper2中商品对应的抢到者List增添该用户id

在这里插入图片描述

public static boolean doSecKill(String uid, String goodsId) {
    //1.uid和goodsId非空判断
    if (uid == null || goodsId == null) return false;

    //2.连接redis
    Jedis jedis = new Jedis("192.168.219.128", 6379);


    //3.拼接key
    //3.1 库存key
    String kcKey = "sk:"+ goodsId + ":qt";
    //3.2 用户key
    String userKey = "sk:"+ goodsId + ":user";

    //4.获取库存,如果库存null,秒杀未开始
    String kc = jedis.get(kcKey);
    if (kc == null) {
        System.out.println("秒杀未开始");
        jedis.close();
        return false;
    }

    //5.判断用户是否重复秒杀
    if(jedis.sismember(userKey, uid)) {
        System.out.println("秒杀成功过l");
        jedis.close();
        return false;
    }

    //6.判断商品数量,小于等于0,秒杀结束
    if (Integer.parseInt(kc) <= 0) {
        System.out.println("秒杀结束");
        jedis.close();
        return false;
    }

    //7.秒杀过程
    //7.1 库存-1
    jedis.decr(kcKey);
    //7.2 加用户
    jedis.sadd(userKey,uid);
    System.out.println("秒杀成功");
    jedis.close();
    return true;
}

上面的例子只是秒杀的具体思路,而现实生活中秒杀肯定是多用户高并发必须考虑并发下的可用性和一致性

2.2 考虑并发

考虑三个人同一个账号购买商品,不加锁没有事务,秒杀结束时会出现负数库存和超出限定个数的秒杀成功者的情况,而且还需要考虑连接超时等问题…

2.2.1 解决连接超时问题

每次的请求都要创建一个Jedis对象将请求打到redis服务器,由于redis是单线程的,后续请求需要排队。

长时间未处理时,本次连接超时,用户秒杀失败,且多次创建redis对象对对服务器而言是很大的浪费

解决连接超时问题,可以采用连接池,类似于Mysql的连接池。

public class JedisPoolUtil {
    private static JedisPool jedisPool = null;
    private JedisPoolUtil() {}

    public static JedisPool getJedisPoolInstance() {
        if (null == jedisPool) {
            synchronized (JedisPoolUtil.class) {
                if (null == jedisPool) {
                    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
                    jedisPoolConfig.setMaxIdle(32);
                    jedisPoolConfig.setMaxTotal(200);
                    jedisPoolConfig.setMaxWaitMillis(100*1000);
                    jedisPoolConfig.setBlockWhenExhausted(true);
                    jedisPoolConfig.setTestOnBorrow(true);

                    jedisPool = new JedisPool(jedisPoolConfig, "192.168.219.128", 6379, 60000);
                }
            }
        }
        return jedisPool;
    }

}

有了连接池,就可以在代码中使用以替代直接连接的方式

//  直接连接redis
Jedis jedis = new Jedis("127.0.0.1", 6379);

// 使用连接池连接redis
JedisPoolUtil jedisPool = JedisPool.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();

3.2.2 超卖问题

Redis中没有使用事务时,多请求操作同一个K对应的数据,极易导致数据混乱

在这里插入图片描述

采用乐观锁watch监控库存的value,将秒杀过程放入multi队列处理

public static boolean doSecKill(String uid, String goodsId) {
    //1.uid和goodsId非空判断
    if (uid == null || goodsId == null) return false;

    //2.连接redis
    //Jedis jedis = new Jedis("192.168.219.128", 6379);
    JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
    Jedis jedis = jedisPoolInstance.getResource();


    //3.拼接key
    //3.1 库存key
    String kcKey = "sk:"+ goodsId + ":qt";
    //3.2 用户key
    String userKey = "sk:"+ goodsId + ":user";

    //开启监视
    jedis.watch(kcKey);
    
    //4.获取库存,如果库存null,秒杀未开始
    String kc = jedis.get(kcKey);
    if (kc == null) {
        System.out.println("秒杀未开始");
        jedis.close();
        return false;
    }

    //5.判断用户是否重复秒杀
    if(jedis.sismember(userKey, uid)) {
        System.out.println("秒杀成功过l");
        jedis.close();
        return false;
    }

    //6.判断商品数量,小于等于0,秒杀结束
    if (Integer.parseInt(kc) <= 0) {
        System.out.println("秒杀结束");
        jedis.close();
        return false;
    }

    //开启事务
    Transaction multi = jedis.multi();
    multi.decr(kcKey);
    multi.sadd(userKey, uid);

    List<Object> exec = multi.exec();

    if (exec == null || exec.size() <= 0) {
        System.out.println("秒杀失败");
        jedis.close();
        return false;
    }

    //7.秒杀过程
    //7.1 库存-1
    //jedis.decr(kcKey);
    //7.2 加用户
    //jedis.sadd(userKey,uid);
    System.out.println("秒杀成功");
    jedis.close();
    return true;
}

3.2.3 库存遗留问题

秒杀还可能出现这样的问题,库存设置为500当整个秒杀快结束时,后到的用户发出请求时发现失败。但此时的库存却还未到0,这就是库存遗留问题,以为卖完了,其实没卖完,出现这样的状况是由于乐观锁导致的,并发来临之际每个请求都能拿到初始版本的数据。当一个请求完成抢购并且修改数据版本号时候,存在其他用户就不能使用该数据。

开始时使用乐观锁watch了库存数值时,此时的库存数据版本是1.0
当秒杀快结束时,有10个人读取到了当前库存值10 版本5.0
假设第一个人的秒杀请求先处理,库存变为9,版本号变为5.1
其他9个人发秒杀请求想改库存数据时,却发现版本号对不上,无法修改库存数
此时秒杀时间结束,就出现了库存仍有遗留的问题
这样的问题很容易想到死锁解决,但redis中并不支持死锁

对此的解决方案可以采用Lua脚本,实质上是Redis利用其单线程的特性,用任务队列的方式解决多任务并发问题

在这里插入图片描述

  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-03-24 00:37:48  更:2022-03-24 00:39:38 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 6:54:31-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码