文章目录
- 一、事务
- 1.1 Multi、Exec、Discard
- 1.2 事务错误处理
- 1.3 事务冲突(乐观锁、悲观锁)
- 1.4 事务三特性
- 1.5 模拟秒杀案例
- 1.5.1 超售、超时问题
- 1.5.2 库存遗留问题
一、事务
Redis 事务是隔离操作,所有命令都会序列化 、顺序执行,不会被其他客户端发送的命令打断。
主要作用:串联多个命令,防止插队。
1.1 Multi、Exec、Discard
首先输入 Multi
命令开启事务,所有命令都会进入队列且不执行(组队阶段),直到输入 Exec
后命令依次执行(运行阶段)。
组队阶段可通过 discard
放弃组队,即销毁队列。
1.2 事务错误处理
组队阶段:命令错误,则整个队列取消。
执行阶段:命令错误,只是当前命令不执行,其他命令正常执行,不回滚
。
1.3 事务冲突(乐观锁、悲观锁)
悲观锁:每次拿数据都会加锁,这样别人来了就会阻塞直到我释放锁。
乐观锁:每次更新数据时判断在此期间是否有人修改过数据,如果没有则更新,否则不操作(适用于多读场景
)。
下图,都监视 cost
,首先对 cost + 1,然后再 + 2。但是只有 + 1成功执行,因为 + 1执行后版本改变,+ 2 时检测到版本改变,不操作。
1.4 事务三特性
1、隔离操作
事务中命令被序列化顺序执行,执行过程中不会被其他命令打断。
2、没有隔离级别
队列中命令没提交前,都不会执行。
3、不保证原子性
事务中一条命令失败,不影响其他命令执行,不回滚。
1.5 模拟秒杀案例
案例介绍:有 n 个商品,给 m 个用户抢购,公布抢中者名单。
@RestController
public class RedisController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/go")
public String testRedis() {
String userId = new Random().nextInt(50000) + "";
String productId = "plane-10086";
// 秒杀过程
if (userId == null || productId == null) {
return "秒杀信息错误";
}
// 用户 key
String userKey = "good:" + productId + ":" + userId;
// 获取库存
int remain = (int) redisTemplate.opsForValue().get(productId);
if (remain == 0) {
return "秒杀未开始";
}
// 判断用户是否重复秒杀
Boolean member = redisTemplate.opsForSet().isMember(userKey, productId);
if (member) {
return "已秒杀成功,不可重复秒杀";
}
// 库存小于1,秒杀结束
if (remain <= 0) {
return "库存不足";
}
// 库存-1,用户加入名单
redisTemplate.opsForValue().decrement(productId);
redisTemplate.opsForSet().add(userKey, productId);
return "秒杀成功:" + userKey;
}
}
使用 ab 模拟并发场景:
高并发场景下,库存变为了负数,发生超售问题
,而且并发线程加大,可能导致 redis 无法处理更多的请求,请求一直等待,出现超时问题
。
1.5.1 超售、超时问题
超时问题:开启连接池
spring:
redis:
host: 175.27.243.243
port: 6379
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
max-wait: 100000ms
超售问题:乐观锁
package com.sugar.redis6.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Random;
@RestController
public class RedisController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/go")
public String Ms() {
String userId = new Random().nextInt(50000) + "";
String productId = "plane";
//set good:plane 10
// 用户 key 商品 key
String userKey = "user:" + userId;
String prodKey = "good:" + productId;
SessionCallback sessionCallback = new SessionCallback<List>() {
@Override
public List execute(RedisOperations operations) throws DataAccessException {
// 监视库存
redisTemplate.watch(prodKey);
// 获取库存, 若库存为0, 表示秒杀未开始
int remain = (int)redisTemplate.opsForValue().get(prodKey);
if (remain == 0) {
System.out.println("秒杀未开始");
return null;
}
// 判断库存
if ((int)redisTemplate.opsForValue().get(prodKey) < 1) {
System.out.println("库存不足啦");
return null;
}
// 判断用户重复秒杀
Boolean member = redisTemplate.opsForSet().isMember(userKey, prodKey);
if (member) {
System.out.println("重复秒杀");
return null;
}
// 开始事务, 抢购开始
redisTemplate.multi();
redisTemplate.opsForValue().decrement(prodKey);
redisTemplate.opsForSet().add(userKey,prodKey);
return redisTemplate.exec();
}
};
List<Object> result = (List<Object>) redisTemplate.execute(sessionCallback);
if (result != null && result.size() != 0) {
System.out.println("秒杀成功" + userKey);
System.out.println("剩余库存:" + redisTemplate.opsForValue().get(prodKey));
}else {
System.out.println("秒杀失败");
}
return null;
}
}
使用 Jmeter 测压
我们发现,100 个线程并发抢购 10 个商品,成功抢购者 6 人。的确解决了超售问题。
但是还有一个问题。100 个人来抢个,只抢购了 6 个商品,这合理吗???这不合理,这就是库存遗留问题。
1.5.2 库存遗留问题
通过 lua
脚本解决抢占问题,本质是利用 redis 的单线程特性,用任务队列解决多任务并发问题
。
其实可以发现,本来是一堆 redis 命令顺序执行,采用 lua 后,只需只需一次,减少网络开销,而且具有原子性
。
local userId=KEYS[1];
local productId=KEYS[2];
local userKey='user:' .. userId;
local prodKey='good:' .. productId;
local userExists=redis.call("sismember",userKey,prodKey);
if tonumber(userExists)==1 then
return 2;
end
local num=redis.call("get",prodKey);
if tonumber(num)<=0 then
return 0;
else
redis.call("decr",prodKey);
redis.call("sadd",userKey,prodKey);
end
return 1;
DefaultRedisScript redisScript = new DefaultRedisScript();
redisScript.setLocation(new ClassPathResource("redis.lua"));
redisScript.setResultType(Long.class);
List<String> list = new ArrayList<>();
list.add(userId);
list.add(productId);
Object result = redisTemplate.execute(redisScript, list);
switch (result.toString()) {
case "1":
System.out.println("秒杀成功");
System.out.println("库存剩余" + redisTemplate.opsForValue().get(prodKey));
case "2":
System.out.println("重复抢购");
case "0":
System.out.println("抢购失败");
}