java秒杀架构设计_秒杀系统设计架构与实现

本文介绍了如何设计和实现一个基于Java的秒杀系统,包括采用Redis解决高并发下的性能问题,利用Nginx做负载均衡,以及使用分布式乐观锁避免超买超卖。详细讲解了架构思路、实现难点及解决方案,涉及Redis、分布式锁和多线程订单持久化等技术。
摘要由CSDN通过智能技术生成

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

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

架构思路

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

494946049047c1b0aa3411fce423c260.png

而且硬盘持久化的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的依赖

org.springframework.boot

spring-boot-starter-data-redis

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 redisTemplate(RedisConnectionFactory connectionFactory) {

RedisTemplate 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 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 +"
库存总量为 "+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 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 members = redisTemplate.opsForSet().members("order"+productId);

List memberList = new ArrayList<>(members);

CountDownLatch countDownLatch = new CountDownLatch(4);

new Thread(() -> {

for (int i = 0; i

secOrderService.save(memberList.get(i));

countDownLatch.countDown();

}

}, "therad1").start();

new Thread(() -> {

for (int i = memberList.size() /4; i

secOrderService.save(memberList.get(i));

countDownLatch.countDown();

}

}, "therad2").start();

new Thread(() -> {

for (int i = memberList.size() /2; i

secOrderService.save(memberList.get(i));

countDownLatch.countDown();

}

}, "therad3").start();

new Thread(() -> {

for (int i = memberList.size() * 3 / 4; 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 findByProductId(String productId);

SecOrder save(SecOrder secOrder);

}

@Service

public class SecOrderServiceImpl implements SecOrderService {

@Autowired

private SecOrderRepository secOrderRepository;

@Override

public List findByProductId(String productId) {

return secOrderRepository.findByProductId(productId);

}

public SecOrder save(SecOrder secOrder){

return secOrderRepository.save(secOrder);

}

}

public interface SecOrderRepository extends JpaRepository {

List 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;

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值