JAVA秒杀系统的简单实现(redis+rabbitmq)

2020-12-29 15:42:14 浏览数 (1)

1.分析

  • 秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。
  • 秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。
  • 秒杀业务流程比较简单,一般就是下订单减库存。

上述三点的主要问题就是在高并发的情况下保证数据的一致性。

2.使用的技术和架构

2.1秒杀架构图

2.2流程

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

2.3流程图

3.准备工作

3.1安装redis cluster

csdn上教程一大堆,这里我就不多赘述了。需要注意的点是,如果使用的是阿里云服务器(centos 7),在安装完后一定要去阿里云服务器控制台添加安全规则,去开放你使用的对应端口号。 https://blog.csdn.net/CFrieman/article/details/83583085

3.2安装RabbitMQ和erlang

还是直接附上链接。需要说明的一点是,在安装erlang时,电脑名称不可以是中文,erlang的版本和rabbitmq的版本一定要对应,负责会安装失败。 https://blog.csdn.net/qq_36505948/article/details/82734133

4.具体实现

4.1SeckillService

代码语言:javascript复制
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

代码语言:javascript复制
@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

代码语言:javascript复制
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可以解决
  • 接口地址隐藏:接口地址传参,保证秒杀接口不是一个固定路径,防止接口被刷,同时也可以有效隐藏秒杀地址。

0 人点赞