秒杀系统设计架构与实现

Posted shoshana-kong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了秒杀系统设计架构与实现相关的知识,希望对你有一定的参考价值。

 

最近https://blog.csdn.net/qq_27631217/article/details/80657271做了一个点餐的平台,其中涉及到一个很重要的问题,活动期间的秒杀系统的实现。

抢购/秒杀是如今很常见的一个应用场景,是高并发编程的一个挑战,在网上也找了一些资料,大部分都是理论,关于java的实现也是很少,就算有也是很简单的demo,为此,决定将此次实现的秒杀系统整理一番,发布出来。

架构思路

Question1: 由于要承受高并发,mysql在高并发情况下的性能下降尤其严重,下图为Mysql性能瓶颈测试。

技术图片

而且硬盘持久化的io操作将耗费大量资源。所以决定采用基于内存操作的redis,redis的密集型io

Question2: 秒杀系统必然是一个集群系统,在硬件不提升的情况下利用nginx做负载均衡也是不错的选择。

实现难点

1. 超买超卖问题的解决。

2. 订单持久化,多线程将订单信息写入数据库

 

 

解决方案

1. 采用redis的分布式乐观锁,解决高并发下的超买超卖问题.

2. 使用countDownLatch作为计数器,将数据四线程写入数据库,订单的持久化过程在我的机器上效率提升了1000倍。

 

进阶方案

1.访问量还是大。系统还是撑不住。

2.防止用户刷新页面导致重复提交。

3.脚本攻击

解决思路:

1.访问量还是过大的话,要看性能瓶颈在哪里,一般来说首先撑不住的是tomcat,考虑优化tomcat,单个tomcat经过实践并发量撑住1000是没有问题的。先搭建tomcat集群,如果瓶颈出现在redis上的话考虑集群redis,这时候消息队列也是必须的,至于采用哪种消息队列框架还是根据实际情况。

2.问题2和问题3其实属于同一个问题。这个问题其实属于网络问题的范畴,和我们的秒杀系统不在一个层面上。因此不应该由我们来解决。很多交换机都有防止一个源IP发起过多请求的功能。开源软件也有不少能实现这点。如linux上的TC可以控制。流行的Web服务器Nginx(它也可以看做是一个七层软交换机)也可以通过配置做到这一点。一个IP,一秒钟我就允许你访问我2次,其他软件包直接给你丢了,你还能压垮我吗?

交换机也不行了呢?

可能你们的客户并发访问量实在太大了,交换机都撑不住了。 这也有办法。我们可以用多个交换机为我们的秒杀系统服务。 原理就是DNS可以对一个域名返回多个IP,并且对不同的源IP,同一个域名返回不同的IP。如网通用户访问,就返回一个网通机房的IP;电信用户访问,就返回一个电信机房的IP。也就是用CDN了! 我们可以部署多台交换机为不同的用户服务。 用户通过这些交换机访问后面数据中心的Redis Cluster进行秒杀作业。

 

我是在springboot + SpringData JPA的环境下实现的系统。引入了spring-data-redis的依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

config包下有两个类

public interface SecKillConfig
String productId = "1234568"; //这是我数据库中的要秒杀的商品id

 这个类的作用主要是配置RedisTemplate,否则使用默认的RedisTemplate会使key和value乱码。

  •  

    @Configuration
    @EnableCaching
    public class RedisConfig
    @Bean
    public CacheManager cacheManager(RedisTemplate<?, ?> redisTemplate)
    CacheManager cacheManager = new RedisCacheManager(redisTemplate);
    return cacheManager;

    // 以下两种redisTemplate自由根据场景选择
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    RedisTemplate<Object, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(connectionFactory);
    Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
    ObjectMapper mapper = new ObjectMapper();
    mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    serializer.setObjectMapper(mapper);
    template.setValueSerializer(serializer);
    //使用StringRedisSerializer来序列化和反序列化redis的key值
    template.setKeySerializer(new StringRedisSerializer());//这两句是关键
    template.setHashKeySerializer(new StringRedisSerializer());//这两句是关键
    template.afterPropertiesSet();
    return template;

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory)
    StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
    stringRedisTemplate.setConnectionFactory(factory);
    return stringRedisTemplate;

下面是util包

public class KeyUtil

public static synchronized String getUniqueKey()
Random random = new Random();
Integer num = random.nextInt(100000);
return num.toString()+System.currentTimeMillis();

public class SecUtils

/*
创建虚拟订单
*/
public static SecOrder createDummyOrder(ProductInfo productInfo)
String key = KeyUtil.getUniqueKey();
SecOrder secOrder = new SecOrder();
secOrder.setId(key);
secOrder.setUserId("userId="+key);
secOrder.setProductId(productInfo.getProductId());
secOrder.setProductPrice(productInfo.getProductPrice());
secOrder.setAmount(productInfo.getProductPrice());
return secOrder;


/*
伪支付
*/
public static boolean dummyPay()
Random random = new Random();
int result = random.nextInt(1000) % 2;
if (result == 0)
return true;

return false;

 

下面是重点,分布式锁的解决

/**
* 分布式乐观锁
*/
@Component
@Slf4j
public class RedisLock

@Autowired
private StringRedisTemplate redisTemplate;

@Autowired
private ProductService productService;

/*
加锁
*/
public boolean lock(String key,String value)

//setIfAbsent对应redis中的setnx,key存在的话返回false,不存在返回true
if ( redisTemplate.opsForValue().setIfAbsent(key,value))
return true;

//两个问题,Q1超时时间
String currentValue = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis())
//Q2 在线程超时的时候,多个线程争抢锁的问题
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue))
return true;


return false;


public void unlock(String key ,String value)
try
String currentValue = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value))
redisTemplate.opsForValue().getOperations().delete(key);

catch(Exception e)
log.error("redis分布上锁解锁异常, ",e);




public SecProductInfo refreshStock(String productId)
SecProductInfo secProductInfo = new SecProductInfo();
ProductInfo productInfo = productService.findOne(productId);
if (productId == null)
throw new SellException(203,"秒杀商品不存在");

try
redisTemplate.opsForValue().set("stock"+productInfo.getProductId(),String.valueOf(productInfo.getProductStock()));
String value = redisTemplate.opsForValue().get("stock"+productInfo.getProductId());
secProductInfo.setProductId(productId);
secProductInfo.setStock(value);
catch(Exception e)
log.error(e.getMessage());

return secProductInfo;



 

分布式锁的实现思路

线程进来之后先执行redis的setnx,若是key存在就返回0,否则返回1.返回1即代表拿到锁,开始执行代码,执行完毕之后将key删除即为解锁。

存在两个问题,有可能存在死锁,就是一个线程执行拿到锁之后,解锁之前的代码时出现bug,导致锁释放不出来,下一个线程进来之后一直等待上一个线程释放锁。解决方案就是加上超时时间,超时过后自行无论执行是否成功都将锁释放出来。但是又会出现第二个问题,在超时的情况下,多个线程同时等待锁释放出来,然后竞争拿到锁,此时又会出现线程不安全现象,解决方案是使用redis的getandset方法,其中一个线程拿到锁之后立即将value值改变,同时将oldvalue与原来的value值比较,这样就保证了多线程竞争锁的安全性。

 

下面是业务逻辑部分的代码。

先是controller

@RestController
@Slf4j
@RequestMapping("/skill")
public class SecKillController

@Autowired
private SecKillService secKillService;

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Resource
private RedisTemplate<String,SecOrder> redisTemplate;

/*
下单,同时将订单信息保存在redis中,随后将数据持久化
*/
@GetMapping("/order/productId")
public String skill(@PathVariable String productId) throws Exception
//判断是否抢光
int amount = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"+productId));
if (amount >= 2000)
return "不好意思,活动结束啦";

//初始化抢购商品信息,创建虚拟订单。
ProductInfo productInfo = new ProductInfo(productId);
SecOrder secOrder = SecUtils.createDummyOrder(productInfo);
//付款,付款时时校验库存,如果成功redis存储订单信息,库存加1
if (!SecUtils.dummyPay())
log.error("付款慢啦抢购失败,再接再厉哦");
return "抢购失败,再接再厉哦";

log.info("抢购成功 商品id=:"+ productId);
//订单信息保存在redis中
secKillService.orderProductMockDiffUser(productId,secOrder);
return "订单数量: "+redisTemplate.opsForSet().size("order"+productId)+
" 剩余数量:"+(2000 - Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"+productId)));

/*
在redis中刷新库存
*/
@GetMapping("/refresh/productId")
public String refreshStock(@PathVariable String productId) throws Exception
SecProductInfo secProductInfo = secKillService.refreshStock(productId);
return "库存id为 "+productId +" <br> 库存总量为 "+secProductInfo.getStock();


Service

@Service
public interface SecKillService

long orderProductMockDiffUser(String productId,SecOrder secOrder);

SecProductInfo refreshStock(String productId);

 

Impl

@Service
@Slf4j
public class SecKillServiceImpl implements SecKillService

@Autowired
private RedisLock redisLock;

@Autowired
private SecOrderService secOrderService;

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Resource
private RedisTemplate<String,SecOrder> redisTemplate;

private static final int TIMEOUT = 10 * 1000;

@Override
public long orderProductMockDiffUser(String productId,SecOrder secOrder)

//加锁 setnx
long orderSize = 0;
long time = System.currentTimeMillis()+ TIMEOUT;
boolean lock = redisLock.lock(productId, String.valueOf(time));
if (!lock)
throw new SellException(200,"哎呦喂,人太多了");

//获得库存数量
int stockNum = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"+productId));
if (stockNum >= 2000)
throw new SellException(150, "活动结束");
else
//仓库数量减一
stringRedisTemplate.opsForValue().increment("stock"+productId,1);
//redis中加入订单
redisTemplate.opsForSet().add("order"+productId,secOrder);
orderSize = redisTemplate.opsForSet().size("order"+productId);
if (orderSize >= 1000)
//订单信息持久化,多线程写入数据库(效率从单线程的9000s提升到了9ms)
Set<SecOrder> members = redisTemplate.opsForSet().members("order"+productId);
List<SecOrder> memberList = new ArrayList<>(members);
CountDownLatch countDownLatch = new CountDownLatch(4);
new Thread(() ->
for (int i = 0; i <memberList.size() /4 ; i++)
secOrderService.save(memberList.get(i));
countDownLatch.countDown();

, "therad1").start();
new Thread(() ->
for (int i = memberList.size() /4; i <memberList.size() /2 ; i++)
secOrderService.save(memberList.get(i));
countDownLatch.countDown();

, "therad2").start();
new Thread(() ->
for (int i = memberList.size() /2; i <memberList.size() * 3 / 4 ; i++)
secOrderService.save(memberList.get(i));
countDownLatch.countDown();

, "therad3").start();
new Thread(() ->
for (int i = memberList.size() * 3 / 4; i <memberList.size(); i++)
secOrderService.save(memberList.get(i));
countDownLatch.countDown();

, "therad4").start();

try
countDownLatch.await();
catch (InterruptedException e)
e.printStackTrace();

log.info("订单持久化完成");


//解锁
redisLock.unlock(productId,String.valueOf(time));
return orderSize;

@Override
public SecProductInfo refreshStock(String productId)
return redisLock.refreshStock(productId);


 

还有一些辅助的service,和实体类,不过多解释,一起贴出来吧,方便大家测试

public interface SecOrderService

List<SecOrder> findByProductId(String productId);

SecOrder save(SecOrder secOrder);

 

@Service
public class SecOrderServiceImpl implements SecOrderService

@Autowired
private SecOrderRepository secOrderRepository;

@Override
public List<SecOrder> findByProductId(String productId)

return secOrderRepository.findByProductId(productId);


public SecOrder save(SecOrder secOrder)

return secOrderRepository.save(secOrder);


 

public interface SecOrderRepository extends JpaRepository<SecOrder,String>

List<SecOrder> findByProductId(String productId);


SecOrder save(SecOrder secOrder);

 

@Entity
@Data
public class ProductInfo

@Id
private String productId;
/**
* 产品名
*/
private String productName;
/**
* 单价
*/
private BigDecimal productPrice;
/**
* 库存
*/
private Integer productStock;
/**
* 产品描述
*/
private String productDescription;
/**
* 小图
*/
private String productIcon;
/**
* 商品状态 0正常 1下架
*/
private Integer productStatus = ProductStatusEnum.Up.getCode();
/**
* 类目编号
*/
private Integer categoryType;

/** 创建日期*/
@JsonSerialize(using = Date2LongSerializer.class)
private Date createTime;
/**更新时间 */
@JsonSerialize(using = Date2LongSerializer.class)
private Date updateTime;

@JsonIgnore
public ProductStatusEnum getProductStatusEnum()
return EnumUtil.getBycode(productStatus,ProductStatusEnum.class);


public ProductInfo(String productId)
this.productId = productId;
this.productPrice = new BigDecimal(3.2);


public ProductInfo()

 

@Data
@Entity
public class SecOrder implements Serializable

private static final long serialVersionUID = 1724254862421035876L;

@Id
private String id;
private String userId;
private String productId;
private BigDecimal productPrice;
private BigDecimal amount;

public SecOrder(String productId)
String utilId = KeyUtil.getUniqueKey();
this.id = utilId;
this.userId = "userId"+utilId;
this.productId = productId;


public SecOrder()


@Override
public String toString()
return "SecOrder" +
"id=‘" + id + ‘\‘‘ +
", userId=‘" + userId + ‘\‘‘ +
", productId=‘" + productId + ‘\‘‘ +
", productPrice=" + productPrice +
", amount=" + amount +
‘‘;

 

@Data
public class SecProductInfo

private String productId;
private String stock;

 

 

 

以上是关于秒杀系统设计架构与实现的主要内容,如果未能解决你的问题,请参考以下文章

互联网 Java 秒杀系统设计与实现

电商商品秒杀系统架构分析与实战

秒杀系统架构解密与防刷设计

八个维度讲解秒杀系统架构分析与实战

秒杀架构

秒杀系统架构设计