Redis事务

当你想做一个抢购程序,利用原始的if语句来实现,会发现显示抢购成功的数量大于预定值。这是由于抢购本身是一个并发操作,系统发出多个并发请求,有慢有快,当一个请求进行判断时可能此时其余情况还没进行货存量-1的操作,导致“成功”数量会大于预订值。

这时候我们需要使用事务

什么是事务

事务(Transaction)是指将一个业务逻辑作为一个整体一起执行。事务其实就是打包一组操作(或者命令)作为一个整体,在事务处理时将顺序执行这些操作,并返回结果,如果其中任何一个环节出错,所有的操作将被回滚。

Redis事务可以保证只有在执行玩玩事务中的所有命令后,才会继续处理此客户端的其他命令。

也就是说只有一个用户可以操作事务当中的数据。

redis中事务从开始到结束经历三个阶段:

redis事务存在四个指令:multi、exec、discard、watch

  • multi 开启一个事务
  • exec 执行一个事务
  • discard 取消一个事务
  • watch 用于并发情况下,为事务提供一个锁,使用watch监控变量如果在执行事务之前,监控项被修改了,那么整个事务被中止。(watch必须写在事务前面,而不是当中)

redisTemplate.execute()是执行器方法,可以执行一系列操作

使用时为:

redisTemplate.execute(new SessionCallback<List<Object>>() {
            @Override
            public List<Object> execute(RedisOperations operations) throws DataAccessException {
            }
        });

当我们需要监听对象时,使用:

redisTemplate.execute(new SessionCallback<List<Object>>() {
    @Override
    public List<Object> execute(RedisOperations operations) throws DataAccessException {
        //监听商品的ID
        operations.watch(id);
    }
});

其中watch()内传入待监视的Redis数据的Key。

事务三阶段:

1.开启事务:

redisTemplate.execute(new SessionCallback<List<Object>>() {
    @Override
    public List<Object> execute(RedisOperations operations) throws DataAccessException {
        //监听商品的ID
        operations.watch(id);

        //开启事务
        operations.multi();//开启事务时不需要参数
    }
});

2.命令入列:

redisTemplate.execute(new SessionCallback<List<Object>>() {
    @Override
    public List<Object> execute(RedisOperations operations) throws DataAccessException {
        //监听商品的ID
        operations.watch(id);

        //开启事务
        operations.multi();

        // 插入一条订单数据。
        // 缓存库的存减 1
        operations.opsForValue().set(idKey, (stock - 1));
        // 数据库的库存减 1
        productDAO.reduceStock(id, 1);
    }
});

execute() 方法中,Redis 的操作不再使用 redisTemplate.opsForValue(),而是使用 operations.opsForValue(),这样系统才知道是同一个事务中的操作。

3.执行事务:

redisTemplate.execute(new SessionCallback<List<Object>>() {
    @Override
    public List<Object> execute(RedisOperations operations) throws DataAccessException {
        //监听商品的ID
        operations.watch(id);

        //开启事务
        operations.multi();

        // 插入一条订单数据。
        // 缓存库的存减 1
        // 数据库的库存减 1

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

operations.exec() 用于执行事务,返回值是 List 列表,存放了每个事务执行结果的标记。事务开启后执行的每个操作,如果成功则放入 true 值作为标记,操作失败则不放入结果标记。

有几个操作就有几个结果标记。因为本演示案例,Redis 只有一个设置库存的操作,所以只有一个标记。

因为事务是要么每个操作都成功,要么都失败,所以一般来说可以简单处理,不用判断 operations.exec() 方法返回值列表中的每个元素是否都为 true,只要判断返回值列表长度大于 0 则表示执行成功。

4. 取消事务

当execute出现异常时会自动取消事务,当我们需要手动取消时使用:

operations.discard();

需要注意的是exec()和discard()是互斥的。

事务使用例子:

例子上部分为判断数据十分需要存入缓存(数据库)

下部分为事务的执行

public Result<Boolean> snappedUp(@RequestParam("id") Long id) {
        //初始化返回数据
        Result result = new Result();
        result.data(true);
        result.setSuccess(true);
        //实现购买代码
        //先去redis查询一下
        Object value = redisTemplate.opsForValue().get(id);
        int stock = 0;
        //如果redis没有则去数据库查询
        if (value == null) {
            //去数据库查询该商品的信息
            ProductDO product = productDAO.selectById(id);
            //将信息缓存到redis里边
            stock = product.getStock();
            redisTemplate.opsForValue().set(product.getId(), stock);
        } else {
            stock = (int)value;
        }

        redisTemplate.execute(new SessionCallback<List<Object>>() {
            @Override
            public List<Object> execute(RedisOperations operations) throws DataAccessException {
                Integer stock = (Integer)operations.opsForValue().get(id);
                //判断该商品的库存是否大于1
                if (Integer.valueOf(stock) >= 1) {
                    //监听商品的名字,redis里边的key
                    operations.watch(id);

                    //开启事务
                    operations.multi();

                    //将该商品的库存自减1
                    operations.opsForValue().set(id, stock - 1);
                    //修改mysql数据库库存数量
                    ProductDO productDO = new ProductDO();
                    productDO.setId(id);
                    productDO.setStock(stock - 1);
                    productDAO.updateStock(productDO);
                    // 执行事务
                    List exec = operations.exec();
                    if (exec.size() > 0) {
                        // TODO:可以有其它业务逻辑,例如插入订单等,视具体需求而定
                        result.setMessage("抢购成功");
                        result.setData(true);
                    } else {
                        result.setMessage("抢购失败");
                        result.setData(false);
                    }

                    return exec;
                } else {
                    result.setMessage("商品库存不足");
                    result.setData(false);

                    return null;
                }
            }
        });

        return result;
    }