JAVA秒杀系统的简单实现(Redis+RabbitMQ)

Lewis
2021-03-14 / 0 评论 / 303 阅读 / 正在检测是否收录...

1、分析

  • 秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。
  • 秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。
  • 秒杀业务流程比较简单,一般就是下订单减库存。
    上述三点的主要问题就是在高并发的情况下保证数据的一致性。

2、使用的技术和架构

2.1 秒杀架构图

km96t16i.png

2.2 实现流程
  • 使用 redis 缓存秒杀的商品信息,秒杀成功后使用消息队列发送订单信息,然后将更新后数据重新写入redis。
  • RabbitMQ监听器在接受到消息后,将订单信息写入数据库。
  • 在秒杀时使用redisson对商品信息上锁。
2.3 流程图

km96uujf.png

3、准备工作

3.1 安装redis cluster

教程一大堆,这里我就不多赘述了,可以参考:https://blog.csdn.net/CFrieman/article/details/83583085

3.2 安装RabbitMQ和erlang

教程一大堆,这里我就不多赘述了,可以参考:https://blog.csdn.net/qq_36505948/article/details/82734133

4、具体实现

4.1 SeckillService
public class SeckillService {

    @Autowired
     private RedisClusterClient rt;

    @Autowired
    private SeckillMapper sm;

    @Autowired
    private RedissonClient redissonClient; // 加锁

    @Autowired
    private RabbitmqSendMessage rsm;

    @Autowired
    private SecorderMapper om;
    
    /**
     * 初始化 ,将mysql中的商品信息缓存到redis中
     * @return
     */
    public List<Seckill> querySeckill() {
        List<Seckill> list =   (List<Seckill>) rt.get("secgoods");
        if(list==null) {
            list = sm.selectByExample(null);
            rt.set("secgoods", list, 60*30);
        }
        return list;
    }

    public boolean queryStartTime(Seckill sec) {
        Date date = new Date();// 比较时间,是否到秒杀时间
        Date startTime = sec.getStarttime();
        // 秒杀活动还未开始
        if (startTime.getTime() > date.getTime()) {
            return false;
        }
        return true;
    }

    // 减库存redis
    public void decreaseStock(String id) {
        int goodsid = Integer.parseInt(id);
        List<Seckill> list =   (List<Seckill>) rt.get("secgoods");
        if (list!=null)
        {
            for (Seckill sec : list)
            {
                if (goodsid==sec.getId())
                {
                    sec.setCount(sec.getCount()-1);
                    //写回redis
                    rt.set("secgoods", list, 60*30);
                    
                    return ;
                }
            }
        }
    }

    //
    public Seckill findSec(String secid) {
         List<Seckill> list =   (List<Seckill>) rt.get("secgoods");
        int id = Integer.parseInt(secid);
        for(Seckill sec:list) {
            if(sec.getId()==id) {
                return sec;
            }
        }
        return null;
    }

    // 开始秒杀
    public String goSeckill(String goodsid, String username) {
        String key = username + ":" + goodsid;

        String secid = goodsid;
        Long value = (Long) rt.get(key);
        if (value != null) {
            return "exist";
        }
        Seckill sec = findSec(secid);
        boolean flag = queryStartTime(sec);
        if (!flag) {
            return "notTime";
        }
        RLock rLock = redissonClient.getLock("miaosha");
        rLock.lock();
        if (sec.getCount() > 0) {
            decreaseStock(goodsid); // 减少库存
            rt.set(key, System.currentTimeMillis(), 60*30);
            Secorder newOrder = new Secorder();
            newOrder.setCreatetime(new Date());
            newOrder.setGoodsid(Integer.parseInt(goodsid));
            newOrder.setStatus("未付款");
            newOrder.setUsername(username);
            String json = JSONObject.toJSONString(newOrder);
            rsm.send(json); // 异步下单
            rLock.unlock(); // 解锁
            return "success";
        } else {
            rLock.unlock();
            return "failed";
        }
    }

    // 写入mysql
    public void saveOrder(String json) {
        Secorder order = JSON.parseObject(json, Secorder.class);
        int n = sm.updateCount(order.getGoodsid());
        int m = om.insert(order);
    }
}
4.2 RabbitmqListenner
@Service
public class RabbitmqListenner implements MessageListener {
    
    @Autowired
    private SeckillService ss;
    
    @Override
    public void onMessage(Message msg) {
        byte[] data = msg.getBody();
        try {
            String     json = new String(data,"utf-8");
            System.out.println(json);
            ss.saveOrder(json);   //将监听到的订单写入MySQL
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
4.3 RabbitmqSendMessage
public class RabbitmqSendMessage {

    @Autowired
    private RabbitTemplate   rt;

    private final String QUEEN_NAME = "MIAOSHA";

    /**
     * 发送消息
     * @param msg
     */
    public void send(String msg)
    {
        rt.convertAndSend(QUEEN_NAME,msg);
    }
}
4.4

以上就是整个业务流程的核心代码,使用redisson保证数据一致性,用rabbitmq异步下单将下单及写数据库这个长操作变成两个短操作。GitHub源码地址,关于数据库建表什么的,大家直接去源码里看吧。

5.优化

限流:使用验证码,请求秒杀接口需要验证图形验证码的正确性,这样也很好的防止脚本的不断访问;
防刷:一个用户对一个路径的访问次数在一定时间内有限制,使用redis可以解决
接口地址隐藏:接口地址传参,保证秒杀接口不是一个固定路径,防止接口被刷,同时也可以有效隐藏秒杀地址。

1

评论

博主关闭了当前页面的评论