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;
}