Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 Redis事务的主要作用就是串联多个命令防止别的命令插队。
组队阶段:从输入multi命令开始,后面输入的任务命令都会依次放入到队列中,但不会执行;
**执行阶段:**及就是从输入exec开始,Redis会将之前的命令队列中的命令依次执行;
**取消事务:**只能在组队的过程中可以通过discard命令来放弃组队。
场景一:组队成功,提交也成功
!
场景二:组队阶段报错,提交失败
**场景三:**组队成功,提交有成功有失败情况
单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
没有隔离级别的概念:队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
不保证原子性 :事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
在实际业务中,有一些场景例如:秒杀、抢车票等等,同一时间多个请求进来,那可能就会存在超卖现象,针对这种情况我们可以使用事务和redis的锁机制来解决这种问题。
**乐观锁(Optimistic Lock):**顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
**悲观锁(Pessimistic Lock):**顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
watch key [key …]-----在执行multi之前,先执行watch key1 key2,可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
unwatch 取消 WATCH 命令对所有 key 的监视。 如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。
http://doc.redisfans.com/transaction/exec.html
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个
Jmeter模拟单个请求
查看redis剩余库存和用户清单:
1.2 模拟高并发500个请求
看着没什么毛病对吧,那如果我把并发加大到500,会出现什么情况呢?
在执行之前先清空redis的数据,点击jmeter执行
控制台输出:
查看redis数据
发现库存 -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
[](javascript:void(0)
2.1 设置10个库存,继续模拟500个并发请求,结果如下:
终于解决超卖问题了,嘻嘻
2.2 那我把库存加大到300个,继续模拟500个并发请求,会出现什么情况呢?
执行Jmeter模拟500个并发,结果如下:
虽然没有超卖问题了,但是有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;