3.Redis事务&&秒杀案例

1. Redis事务

1.1 Redis的事务定义

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

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

三个阶段:

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

1.2 Multi、Exec、discard

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

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

3.Redis事务&&秒杀案例_第1张图片

案例:

组队成功,提交成功

3.Redis事务&&秒杀案例_第2张图片

组队阶段报错,提交失败

3.Redis事务&&秒杀案例_第3张图片

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

3.Redis事务&&秒杀案例_第4张图片

1.3 事务的错误处理

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

3.Redis事务&&秒杀案例_第5张图片

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

3.Redis事务&&秒杀案例_第6张图片

1.4 WATCH

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

1.4.1 悲观锁和乐观锁

  • 悲观锁

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

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

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

    3.Redis事务&&秒杀案例_第7张图片

  • 乐观锁

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

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

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

    3.Redis事务&&秒杀案例_第8张图片

1.4.2 具体过程

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

3.Redis事务&&秒杀案例_第9张图片

客户端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

3.Redis事务&&秒杀案例_第10张图片

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对应的数据,极易导致数据混乱

3.Redis事务&&秒杀案例_第11张图片

采用乐观锁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利用其单线程的特性,用任务队列的方式解决多任务并发问题

3.Redis事务&&秒杀案例_第12张图片

你可能感兴趣的:(#,Redis,redis)