Redis的事务

1)Redis的事务

1.1 Redis事务的定义:

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

1.2 Multi、Exec、discard命令

组队阶段:从输入multi命令开始,后面输入的任务命令都会依次放入到队列中,但不会执行;

**执行阶段:**及就是从输入exec开始,Redis会将之前的命令队列中的命令依次执行;

**取消事务:**只能在组队的过程中可以通过discard命令来放弃组队。

1.3 实操如下:

场景一:组队成功,提交也成功

!

场景二:组队阶段报错,提交失败

Redis的事务_第1张图片

**场景三:**组队成功,提交有成功有失败情况

Redis的事务_第2张图片

1.4 Redis事务三特性

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

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

不保证原子性 :事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

(2)Redis的事务锁机制

2.1 Redis的锁机制

在实际业务中,有一些场景例如:秒杀、抢车票等等,同一时间多个请求进来,那可能就会存在超卖现象,针对这种情况我们可以使用事务和redis的锁机制来解决这种问题。

**乐观锁(Optimistic Lock):**顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

**悲观锁(Pessimistic Lock):**顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

2.2 watch和unwatch的命令

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

unwatch 取消 WATCH 命令对所有 key 的监视。 如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。

http://doc.redisfans.com/transaction/exec.html

(3)秒杀案例

  1. 使用Redis解决计数器和人员记录的事务操作
  2. 模拟:单个请求到并发秒杀(使用工具JMeter模拟测试)
  3. 超卖问题:利用事务和乐观锁淘汰用户,解决超卖问题
  4. 模拟:加大库存,会存在秒杀结束却还有库存
  5. 使用LUA脚本解决库存剩余问题

1.使用Redis解决计数器和人员记录的事务操作

写个秒杀测试类如下:

[复制代码](javascript:void(0)

/**
 * 秒杀案例,一个用户只能秒杀成功一次
 */
@RestController
@RequestMapping("/testRedisSeckill")
public class TestRedisSeckillController {

    @Autowired
    private RedisTemplate redisTemplate;


    @GetMapping("/doSeckill")
    public boolean doSeckill() throws IOException {
        String usrId = new Random().nextInt(50000) + "";
        return doSeckillFun(usrId, "20210731");
    }
}

[复制代码](javascript:void(0)

[复制代码](javascript:void(0)

/**
     * 秒杀过程1(高并发下会超卖)
     *
     * @param usrId 用户id
     * @param atcId 活动id
     * @return
     * @throws IOException
     */
    private boolean doSeckillFun(String usrId, String atcId) throws IOException {
        //1.参数校验
        if (usrId == null || atcId == null) return false;

        //2.设置Redis值(库存key== atcId:stock, 秒杀成功用户key== atcId:userId)
        String stockKey = atcId + ":stock";
        String userIdKey = atcId + ":userId";

        //3.获取库存,如果库存是空,秒杀还没开始
        Object stock = redisTemplate.opsForValue().get(stockKey);//获取库存
        if (stock == null) {
            System.out.println("别着急,秒杀还没开始呢!!");
            return false;
        }

        //4.判断用户是否重复秒杀操作(Set类型操作)
        if (redisTemplate.opsForSet().isMember(userIdKey, usrId)) {
            System.out.println("你已经秒杀成功了,不能重复秒杀");
            return false;
        }

        //5.判断库存数量,小于1,秒杀结束
        int stock1 = (int) stock;
        if (stock1 < 1) {
            System.out.println("秒杀结束了。。。");
            return false;
        }

        //6.秒杀过程(库存减1,把秒杀成功用户添加到用户清单)
        redisTemplate.opsForSet().add(userIdKey, usrId);
        redisTemplate.opsForValue().decrement(stockKey);//库存-1
        System.out.println("恭喜你!秒杀成功了!");
        return true;
    }

[复制代码](javascript:void(0)

1.1 模拟场景1:单个请求

先设置库存10个

img

Jmeter模拟单个请求

Redis的事务_第3张图片

Redis的事务_第4张图片

Redis的事务_第5张图片

查看redis剩余库存和用户清单:

Redis的事务_第6张图片

1.2 模拟高并发500个请求

看着没什么毛病对吧,那如果我把并发加大到500,会出现什么情况呢?

Redis的事务_第7张图片

在执行之前先清空redis的数据,点击jmeter执行

img

控制台输出:

Redis的事务_第8张图片

查看redis数据

Redis的事务_第9张图片

发现库存 -190,出现超卖了,所以我们的场景1的代码在高并发的情况下会出现超卖的问题,那么针对这个问题我们需要使用乐观锁来解决

2.使用乐观锁来解决

代码如下:

[复制代码](javascript:void(0)

/**
     * 秒杀过程2(乐观锁解决超卖问题)
     *
     * @param usrId 用户id
     * @param atcId 活动id
     * @return
     * @throws IOException
     */
    private boolean doSeckillFun(String usrId, String atcId) throws IOException {
        //1.参数校验
        if (usrId == null || atcId == null) return false;

        //2.设置Redis值(库存key== atcId:stock, 秒杀成功用户key== atcId:userId)
        String stockKey = atcId + ":stock";
        String userIdKey = atcId + ":userId";

        //通过 SessionCallback,保证所有的操作都在同一个 Session 中完成
        //更常见的写法仍是采用 RedisTemplate 的默认配置,即不开启事务支持。
        // 但是,我们可以通过使用 SessionCallback,该接口保证其内部所有操作都是在同一个Session中
        SessionCallback callback = new SessionCallback() {

            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                //3. 打开事务支持
                //redisTemplate.setEnableTransactionSupport(true);

                //4.增加乐观锁进行对库存的监视
                operations.watch(stockKey);

                //5.获取库存,如果库存是空,秒杀还没开始
                Object stock = operations.opsForValue().get(stockKey);//获取库存
                if (stock == null) {
                    System.out.println("别着急,秒杀还没开始呢!!");
                    return false;
                }

                //6.判断用户是否重复秒杀操作(Set类型操作)
                if (operations.opsForSet().isMember(userIdKey, usrId)) {
                    System.out.println("你已经秒杀成功了,不能重复秒杀");
                    return false;
                }

                //7.判断库存数量,小于1,秒杀结束
                int stock1 = (int) stock;
                if (stock1 < 1) {
                    System.out.println("秒杀结束了。。。");
                    return false;
                }

                //8. 增加事务
                operations.multi();

                //9.秒杀过程
                operations.opsForValue().decrement(stockKey);//库存-1
                operations.opsForSet().add(userIdKey, usrId);//把秒杀成功用户添加到用户清单

                //10.执行事务
                List list = operations.exec();

                //11.判断事务提交是否失败
                if (list == null || list.size() == 0) {
                    System.out.println("秒杀失败");
                    return false;
                }

                System.out.println("恭喜你!秒杀成功了!");
                return true;
            }
        };

        return (boolean) redisTemplate.execute(callback);
    }
 
  

[复制代码](javascript:void(0)

img

2.1 设置10个库存,继续模拟500个并发请求,结果如下:

Redis的事务_第10张图片

Redis的事务_第11张图片

终于解决超卖问题了,嘻嘻

2.2 那我把库存加大到300个,继续模拟500个并发请求,会出现什么情况呢?

Redis的事务_第12张图片

执行Jmeter模拟500个并发,结果如下:

img

虽然没有超卖问题了,但是有500个请求却还剩余102个库存,那么就有下边的lua解决库存遗留问题。

3. LUA脚本在Redis中的优势 :将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。 LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。 但是注意redis的lua脚本功能,只有在Redis 2.6 以上的版本才可以使用。 利用lua脚本淘汰用户,解决超卖问题。 redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。

使用Lua脚本解决库存遗留的问题,代码如下:

[复制代码](javascript:void(0)

 /**
     * 秒杀过程3(LUA解决库存剩余问题)
     *
     * @param usrId 用户id
     * @param atcId 活动id
     * @return
     * @throws IOException
     */
    private boolean doSeckillFun(String usrId, String atcId) throws IOException {

        String luaScript = "local userId=KEYS[1];\r\n" +
                "local stockKey=KEYS[2];\r\n" +
                "local userIdKey=KEYS[3];\r\n" +
                "local userExists=redis.call(\"sismember\",userIdKey,userId); \r\n" +
                "if tonumber(userExists)==1 \r\n" +
                "then \r\n" +
                "  return 2;\r\n" +
                "end \r\n" +
                "local num= redis.call(\"get\" ,stockKey);\r\n" +
                "if tonumber(num)<=0 then   return 0;\r\n" +
                "else \r\n " +
                " redis.call(\"decr\",stockKey);\r\n" +
                "redis.call(\"sadd\",userIdKey,userId);\r\n" +
                "end \r\n" +
                "return 1;";

        // 指定 lua 脚本,并且指定返回值类型
        // (为什么返回值不用 Integer 接收而是用 Long。这里是因为 spring-boot-starter-data-redis 提供的返回类型里面不支持 Integer。)
        DefaultRedisScript redisScript = new DefaultRedisScript<>(luaScript, Long.class);
        List keys = new ArrayList<>();
        keys.add(usrId);
        keys.add(atcId + ":stock");
        keys.add(atcId + ":userId");
        // 参数一:redisScript,参数二:key列表,参数三:arg(可多个)
        Long result = (Long) redisTemplate.execute(redisScript, keys);
        if (0 == result) {
            System.out.println("秒杀结束了。。。");
        } else if (1 == result) {
            System.out.println("恭喜你!秒杀成功了!");
            return true;
        } else if (2 == result) {
            System.out.println("你已经秒杀成功了,不能重复秒杀");
        } else {
            System.out.println("秒杀异常啦~");
        }

        return false;
    }

[复制代码](javascript:void(0)

lua脚本:

[复制代码](javascript:void(0)

local userId=KEYS[1];
local stockKey=KEYS[2];
local userIdKey=KEYS[3];
local userExists=redis.call("sismember",userIdKey,userId); 
if tonumber(userExists)==1
then
  return 2;
end
local num= redis.call("get" ,stockKey);
if tonumber(num)<=0 then   return 0;
else
 redis.call("decr",stockKey);
redis.call("sadd",userIdKey,userId);
end
return 1;

``
local userId=KEYS[1];
local stockKey=KEYS[2];
local userIdKey=KEYS[3];
local userExists=redis.call(“sismember”,userIdKey,userId);
if tonumber(userExists)==1
then
return 2;
end
local num= redis.call(“get” ,stockKey);
if tonumber(num)<=0 then return 0;
else
redis.call(“decr”,stockKey);
redis.call(“sadd”,userIdKey,userId);
end
return 1;


你可能感兴趣的:(redis,缓存,数据库)