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