redis实现抢票功能
在上一小节我们学习了使用redis实现分布式锁,分布式锁一般应用在操作互斥的操作mysql或者其他第三方资源。今天我们要了解的是使用redis实现秒杀抢购功能,在我们平常接触的618、双11、12306买票等等,都涉及到大量的并发访问。而要很好的保护后台系统,我们就需要就要尽量少的把请求发到mysql等数据上,而是需要把商品等信息提前缓存到redis,通过redis来时间商品库存的递减。下面我们就来模拟实现redis抢票的功能。
基础
实现redis的抢票功能,我们需要先了解几个redis的事务及命令。我们知道redis提供了事务的功能,但是redis事务不提供回滚的功能,具体原因是:
(1) redis的命令执行的失败一般是由于语法错误或者类型错误,这意味着在实际操作中,失败的命令是编程错误的结果,并且是在开发过程中很可能检测到的一种错误,而不是在生产中。
(2) redis内部简化,速度更快,不需要回滚通常回滚并不能避免编程错误。例如,如果一个查询将一个键增加2而不是1,或者增加了错误的键,那么回滚机制就没有办法提供帮助。
redis提供事务相关的命令:
MULTI: 开启一个事务;
EXEC: 事务执行,将一次性执行事务内的所有命令
DISCARD: 取消事务
WATCH: 监视一个或多个键,如果事务执行前某个键发生了改动,那么事务也会被打断
UNWATCH: 取消WATCH命令对所有键的监视
秒杀、抢票其实是使用redis的WATCH和MULTI实现的乐观锁机制。
redis实现事务是基于COMMAND队列的,如果redis没有开启事务,那么任何的COMMAND都会立即执行并返回结果。如果开启了事务,COMMAND命令会放到队列中,并且返回排队的状态QUEUED,只有调用EXEC,才会执行COMMAND队列中的命令。
我们使用两个redis客户端模拟抢票:
分析说明:
- T1时刻客户端1执行SET ticket 1把票设置为1
- T2时刻客户端1和客户端2分别执行WATCH ticket 来监视ticket键的变化
- T3时刻客户端1和客户端2分别执行MUlTI开始事务
- T4时刻客户端1和客户端2分别执行抢票动作,把ticket设置为0
- T5时刻客户端2先执行,把ticket置为0,表示抢到票
- T6时刻客户端1再执行,由于WATCH的监视的机制,ticket已经发生了变化,所以客户端1的事务被打断,返回nil。
下面我们就使用java来编写程序实现抢票功能,环境搭建见redis实现分布式锁
抢票线程实现
@Slf4j
class BuyTicketThread implements Runnable {
private int userId;
private JedisPool jedisPool;
private String key;
//用来做线程间同步
private CountDownLatch countDownLatch;
public BuyTicketThread(int userId,JedisPool jedisPool,String KEY,CountDownLatch countDownLatch) {
this.userId = userId;
this.jedisPool = jedisPool;
this.key = KEY;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
Jedis jedis = jedisPool.getResource();
while(true) {
try{
//监视票数
jedis.watch(key);
// 查看票数
int c = Integer.valueOf(jedis.get(key));
if(c > 0) {
// 开启事务
Transaction tx = jedis.multi();
c = c -1;
tx.set(key, String.valueOf(c));
List<Object> results = tx.exec();
if(results != null){
log.info("用户{}抢票成功,当前票数{}",userId,c);
break;
}else {
//返回null表示事务执行失败
log.info("用户{}抢票失败,重试一次",userId);
continue;
}
}else {
log.info("用户{} 抢票失败,票卖完了",userId);
break;
}
}catch (Exception e) {
e.printStackTrace();
}finally {
jedis.unwatch();
}
}
countDownLatch.countDown();
jedis.close(); // 需要调用,否则下次调用会出现获取不到连接(阻塞)卡死现象 jedisPool.getResource()
}
}
开启线程抢票接口
本节实现4个线程抢2张票,最终只能有2个线程抢到票,也可以改更多线程抢更多的票。
@RequestMapping("/buyTickets")
@ResponseBody
public String buyTickets() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(4);
String KEY = "ticket_count";
ExecutorService executorService = Executors.newFixedThreadPool(4);
jedisPool.getResource().set(KEY,String.valueOf(2));
for(int i = 0;i <4;i++) {
executorService.submit(new BuyTicketThread(i,jedisPool,KEY,countDownLatch));
}
countDownLatch.await();
executorService.shutdown();
System.out.println("票卖完了..................");
return "完毕";
}
4个线程抢2张票程序执行结果如下: