作者:小亮
个人主页:https://blog.csdn.net/qq_27631217
知音专栏
程序员的出路
写程序时该追求什么,什么是次要的?
如何准备Java初级和高级的技术面试
算法的力量,李开复聊算法的重要性
最近做了一个点餐的平台,其中涉及到一个很重要的问题,活动期间的秒杀系统的实现。
抢购/秒杀是如今很常见的一个应用场景,是高并发编程的一个挑战,在网上也找了一些资料,大部分都是理论,关于java的实现也是很少,就算有也是很简单的demo,为此,决定将此次实现的秒杀系统整理一番,发布出来。
架构思路
Question1: 由于要承受高并发,mysql在高并发情况下的性能下降尤其严重,下图为Mysql性能瓶颈测试。
而且硬盘持久化的io操作将耗费大量资源。所以决定采用基于内存操作的redis,redis的密集型io
Question2: 秒杀系统必然是一个集群系统,在硬件不提升的情况下利用nginx做负载均衡也是不错的选择。
实现难点
超买超卖问题的解决。
- 订单持久化,多线程将订单信息写入数据库
解决方案
采用redis的分布式乐观锁,解决高并发下的超买超卖问题.
- 使用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@EnableCachingpublic 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@Slf4jpublic 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 SecKillServic���ϳ���,���ﳹ��e 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
@Servicepublic interface SecKillService { long orderProductMockDiffUser(String productId,SecOrder secOrder); SecProductInfo refreshStock(String productId);}
Impl
@Service@Slf4jpublic 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);}
@Servicepublic 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@Datapublic 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@Entitypublic 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 + '}'; }}
@Datapublic class SecProductInfo { private String productId; private String stock;}