redis实现多用户抢票

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客户端模拟抢票:


image.png

分析说明:

  • 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张票程序执行结果如下:


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容