1.分析

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

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

2.使用的技术和架构

2.1秒杀架构图

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

2.2流程

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

2.3流程图

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

3.准备工作

3.1安装redis cluster

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

3.2安装RabbitMQ和erlang

还是直接附上链接。需要说明的一点是,在安装erlang时,电脑名称不可以是中文,erlang的版本和rabbitmq的版本一定要对应,负责会安装失败。

4.具体实现

4.1SeckillService

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