All Projects → Grootzz → Seckill

Grootzz / Seckill

Spring Boot+MySQL+Redis+RabbitMQ的高性能高并发商品秒杀系统设计与优化

Programming Languages

java
68154 projects - #9 most used programming language

Projects that are alternatives of or similar to Seckill

Mall
mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现,采用Docker容器化部署。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。
Stars: ✭ 54,797 (+53100.97%)
Mutual labels:  mysql, redis, spring-boot, rabbitmq
Cookbook
🎉🎉🎉JAVA高级架构师技术栈==任何技能通过 “刻意练习” 都可以达到融会贯通的境界,就像烹饪一样,这里有一份JAVA开发技术手册,只需要增加自己练习的次数。🏃🏃🏃
Stars: ✭ 428 (+315.53%)
Mutual labels:  mysql, redis, spring-boot, rabbitmq
Mall Swarm
mall-swarm是一套微服务商城系统,采用了 Spring Cloud Hoxton & Alibaba、Spring Boot 2.3、Oauth2、MyBatis、Docker、Elasticsearch、Kubernetes等核心技术,同时提供了基于Vue的管理后台方便快速搭建系统。mall-swarm在电商业务的基础集成了注册中心、配置中心、监控中心、网关等系统功能。文档齐全,附带全套Spring Cloud教程。
Stars: ✭ 7,874 (+7544.66%)
Mutual labels:  mysql, redis, rabbitmq
Springbootunity
rabbitmq、redis、scheduled、socket、mongodb、Swagger2、spring data jpa、Thymeleaf、freemarker etc. (muti module spring boot project) (with spring boot framework,different bussiness scence with different technology。)
Stars: ✭ 845 (+720.39%)
Mutual labels:  mysql, redis, spring-boot
Supermarket
设计精良的网上商城系统,包括前端、后端、数据库、负载均衡、数据库缓存、分库分表、读写分离、全文检索、消息队列等,使用SpringCloud框架,基于Java开发。该项目可部署到服务器上,不断完善中……
Stars: ✭ 1,278 (+1140.78%)
Mutual labels:  mysql, redis, rabbitmq
Shirojwt
API SpringBoot + Shiro + Java-Jwt + Redis(Jedis)
Stars: ✭ 503 (+388.35%)
Mutual labels:  mysql, redis, spring-boot
Taroco
整合Nacos、Spring Cloud Alibaba,提供了一系列starter组件, 同时提供服务治理、服务监控、OAuth2权限认证,支持服务降级/熔断、服务权重,前端采用vue+elementUI+webpack,可以很好的解决转向Spring Cloud的一系列问题。
Stars: ✭ 545 (+429.13%)
Mutual labels:  mysql, redis, spring-boot
Socket Io
基于Hyperf微服务协程框架开发的sokcet-io分布式系统
Stars: ✭ 38 (-63.11%)
Mutual labels:  mysql, redis, rabbitmq
Gnomock
Test your code without writing mocks with ephemeral Docker containers 📦 Setup popular services with just a couple lines of code ⏱️ No bash, no yaml, only code 💻
Stars: ✭ 398 (+286.41%)
Mutual labels:  mysql, redis, rabbitmq
Goods Seckill
高性能电商秒杀解决方案,redis预减库存,消息队列异步下单,订单防重,订单防刷,秒杀接口地址隐藏,数学公式验证码
Stars: ✭ 61 (-40.78%)
Mutual labels:  mysql, redis, spring-boot
Phalcon Vm
Vagrant configuration for PHP7, Phalcon 3.x and Zephir development.
Stars: ✭ 43 (-58.25%)
Mutual labels:  mysql, redis, rabbitmq
Mall Learning
mall学习教程,架构、业务、技术要点全方位解析。mall项目(40k+star)是一套电商系统,使用现阶段主流技术实现。涵盖了SpringBoot 2.3.0、MyBatis 3.4.6、Elasticsearch 7.6.2、RabbitMQ 3.7.15、Redis 5.0、MongoDB 4.2.5、Mysql5.7等技术,采用Docker容器化部署。
Stars: ✭ 10,236 (+9837.86%)
Mutual labels:  mysql, redis, rabbitmq
Pdf
编程电子书,电子书,编程书籍,包括C,C#,Docker,Elasticsearch,Git,Hadoop,HeadFirst,Java,Javascript,jvm,Kafka,Linux,Maven,MongoDB,MyBatis,MySQL,Netty,Nginx,Python,RabbitMQ,Redis,Scala,Solr,Spark,Spring,SpringBoot,SpringCloud,TCPIP,Tomcat,Zookeeper,人工智能,大数据类,并发编程,数据库类,数据挖掘,新面试题,架构设计,算法系列,计算机类,设计模式,软件测试,重构优化,等更多分类
Stars: ✭ 12,009 (+11559.22%)
Mutual labels:  mysql, redis, rabbitmq
Testcontainers Spring Boot
Container auto-configurations for spring-boot based integration tests
Stars: ✭ 460 (+346.6%)
Mutual labels:  redis, spring-boot, rabbitmq
Bifrost
Bifrost ---- 面向生产环境的 MySQL 同步到Redis,MongoDB,ClickHouse,MySQL等服务的异构中间件
Stars: ✭ 701 (+580.58%)
Mutual labels:  mysql, redis, rabbitmq
Javaok
必看!java后端,亮剑诛仙。java发展路线技术要点。
Stars: ✭ 867 (+741.75%)
Mutual labels:  mysql, redis, spring-boot
Fxshop
基于SpringBoot+SpringCloud微服务的商城项目(demo版 不可用于生产)
Stars: ✭ 82 (-20.39%)
Mutual labels:  redis, spring-boot, rabbitmq
His
HIS英文全称 hospital information system(医院信息系统http://59.110.234.89:9999/swagger-ui.html ),医疗信息就诊系统,系统主要功能按照数据流量、流向及处理过程分为临床诊疗、药品管理、财务管理、患者管理。诊疗活动由各工作站配合完成,并将临床信息进行整理、处理、汇总、统计、分析等。本系统包括以下工作站:门诊医生工作站、药房医生工作站、医技医生工作站、收费员工作站、对帐员工作站、管理员工作站。需求为东软提供的云医院。
Stars: ✭ 359 (+248.54%)
Mutual labels:  redis, spring-boot, rabbitmq
Full Stack Notes
全栈工程师手册
Stars: ✭ 366 (+255.34%)
Mutual labels:  mysql, redis, rabbitmq
Nagios Plugins
450+ AWS, Hadoop, Cloud, Kafka, Docker, Elasticsearch, RabbitMQ, Redis, HBase, Solr, Cassandra, ZooKeeper, HDFS, Yarn, Hive, Presto, Drill, Impala, Consul, Spark, Jenkins, Travis CI, Git, MySQL, Linux, DNS, Whois, SSL Certs, Yum Security Updates, Kubernetes, Cloudera etc...
Stars: ✭ 1,000 (+870.87%)
Mutual labels:  mysql, redis, rabbitmq

高性能高并发商品秒杀系统设计与优化

[TOC]

项目简介

本项目主要是模拟应对大并发场景下,如何完成商品的秒杀,以及针对秒杀场景下为应对大并发所做的优化。

项目的技术结构如下图所示:

项目技术ç"“æž„.png

秒杀场景下主要解决的问题:

  • 分布式会话
  • 用户登录、商品列表、商品详情、订单详情模块
  • 缓存优化
  • 系统压测,测试系统的QPS
  • 信息队列
  • 接口安全

本项目的分布式改造版本:https://github.com/Grootzz/dis-seckill

快速启动

  1. 第一步;克隆仓库到本地

    git clone https://github.com/Grootzz/seckill
    
  2. 第二步;构建工程

    cd seckill
    mvn clean package
    
  3. 第三步;启动

    java -jar /target seckill-1.0-SNAPSHOT.jar
    

访问入口:http://localhost:8080/login/to_login

初始账号/密码:18342390420/000000

数据库设计

这里的数据库设计只是为了模拟秒杀场景,实际的数据库会复杂很多。需要注意的是,因为参与秒杀的只有部分商品,所以单独建立一个miaosha_goods存储于秒杀商品相关的字段。

  • 秒杀用户表:miaosha_user
  • 商品表:goods
  • 参与秒杀的商品表:miaosha_goods
  • 秒杀订单表:miaosha_order
  • 订单表:order_info

数据库设计.png

登录实现

登录部分主要有以下几个部分:

  • 明文密码两次MD5处理

  • JSR303参数检验和全局异常处理器

  • 分布式Session

明文密码两次MD5处理

  • 客户端:C_PASS=MD5(明文+固定salt)
  • 服务端:S_PASS=MD5(C_PASS+随机salt)

加密:出于安全考虑

第一次 (在前端加密,客户端):密码加密是(明文密码+固定盐值)生成md5用于传输,目的,由于http是明文传输,当输入密码若直接发送服务端验证,此时被截取将直接获取到明文密码,获取用户信息。

加盐值是为了混淆密码,原则就是明文密码不能在网络上传输。

第二次:在服务端再次加密,当获取到前端发送来的密码后。通过MD5(密码+随机盐值)再次生成密码后存入数据库。

防止数据库被盗的情况下,通过md5反查,查获用户密码。方法是盐值会在用户登陆的时候随机生成,并存在数据库中,这个时候就会获取到。

第二次的目的: 黑客若是同时黑掉数据库,通过解析前端js文件,知道如果md5加密的过程,就知道此时用户的密码。

但是此时我们要是在后端加入随机盐值和传输密码的md5组合,黑客是无法知道通过后端密码加密过程的,从而无法知道密码。

JSR303参数检验和全局异常处理器

JSR303 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解。如@NotNull@Email@Max等。

在这个系统中,我们自定义了一个注解@IsMobile完成手机号码的参数检验,@IsMobile的校验处理器为IsMobileValidator

定义一个全局异常GlobalException和全局异常处理器GlobalExceptionHandler,可以完成系统异常的捕获和异常的统一处理。

分布式Session

在用户登录成功之后,将用户信息存储在redis中,然后生成一个token返回给客户端,这个token为存储在redis中的用户信息的key,这样,当客户端第二次访问服务端时会携带token,首先到redis中获取查询该token对应的用户使用是否存在,这样也就不用每次到数据库中去查询是不是该用户了,从而减轻数据库的访问压力。

秒杀功能的实现

  • 数据库设计

  • 商品列表页

  • 商品详情页

  • 订单详情页

页面优化技术

  • 页面级缓存+URL缓存+对象缓存
  • 页面静态化,前后端分离
  • 静态资源优化
  • CDN优化

页面级缓存+URL缓存+对象缓存

所谓页面缓存,指的是对于服务端的请求,不直接从系统中获取页面资源,而是先从缓存中获取页面资源,如果缓存中不存在页面资源,则系统将渲染页面并存储页面到缓存中,然后将页面返回。

来看商品列表页的请求过程;请求到服务端,服务端查询数据库中的商品列表信息然后存储在Model对象中,Thymeleaf页面获取在Model对象中的商品列表信息然后动态渲染,再返回给客户端。如果每次请求都做这样的工作,势必会对服务器和系统造成一定的压力(系统的压力主要来源于每次Thymeleaf页面获取在Model对象的信息都要渲染一次),所以可以做一个页面级的缓存,减轻数据库和系统的压力。

在本项目中,我们对商品列表页做一个缓存,因为商品列表页的数据相对表话不是太频繁,所以将其缓存在redis中,这样不用每次都查询数据库中的商品信息,然后再使用Thymeleaf渲染返回,而是直接从redis中返回。另外,由于商品列表页请求返回的是html,所以这里使用ThymeleafViewResolver手动渲染页面,这样就可以将页面直接通过系统返回给客户端。(详细过程在edu.uestc.controller.GoodsListController#toList中)。

而所谓URL缓存,实际上和页面缓存是一样的,在本项目中,我们对商品详情页做了缓存,商品详情页的请求需要goodsId,也就是说,对每一个goodsId都做了一个缓存,其他的和商品列表页的缓存思路是一致的,只不过商品取详情页是需要动态的根据goodsId来取。

通过上面的缓存差异可知,URL缓存和页面缓存的不同之处在于,URL缓存需要根据URL中的参数动态地取缓存,而页面缓存则不需要。

一般来讲,URL缓存和页面缓存的缓存时间都比较短。在本项目中,我们设置商品详情页商品列表页的缓存时间为60s。

对象缓存是一种更细粒度的缓存,顾名思义就是对对象就行缓存,在本项目中,我们将edu.uestc.service.MiaoshaUserService#getMiaoshaUserById获取的对象进行了缓存,另外,edu.uestc.service.MiaoshaUserService#getMisaoshaUserByToken获取的对象也做了一个缓存。edu.uestc.service.MiaoshaUserService#updatePassword方法同样做了对象级的缓存,但是值得注意的是,这个方法里对缓存中的数据进行了更改,因此,需要将将更改的对象先从缓存中取出,然后删除缓存中对应的数据,然后在将新的数据更新到数据库中,再将数据缓存到redis中。对于缓存的更新,可以参考缓存更新方法

为什么要先删除缓存在写入缓存呢?因为如果不删除,以前的请求仍然可以访问通过原来的token访问到以前的数据(这里的token可以查看edu.uestc.service.MiaoshaUserService#updatePassword的逻辑得知),除了造成数据的不一致还会有安全问题,所以需要删除以前的缓存在写入新的缓存。

页面静态化,前后端分离

页面静态化指的是将页面直接缓存到客户端。常用的技术有Angular.jsVue.js

其实现方式就是通过ajax异步请求服务器获取动态数据,对于非动态数据部分缓存在客户端,客户端通过获取服务端返回的json数据解析完成相应的逻辑。

在本项目中,我们对商品详情页订单详情页做了一个静态化处理。

对于商品详情页,异步地从服务端获取商品详情信息,然后客户端完成页面渲染工作。除此之外,对于秒杀信息的获取也是通过异步获取完成的。例如,当秒杀开始时,用户执行秒杀动作,客户端就会轮询服务器获取秒杀结果。而不需要服务器直接返回页面。

而对于订单详情页,实际上也是同样的思路。

静态资源优化

  • JS/CSS压缩,减少流量。客户端完成解压工作。
  • 多个JS/CSS组合,减少连接数。一次TCP连接完成多个HTTP交互。
  • CDN就近访问。

CDN优化

CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储和分发技术。

接口优化

  • Redis预减库存减少数据库的访问

  • 内存标记减少redis访问

  • 请求先入队缓冲,异步下单,增强用户体验

  • RabbitMQ安装与Spring Boot集成

  • Nginx水平扩展

  • 压测

Redis预减库存减少数据库的访问

核心思想:减少对数据库的访问。

在做秒杀时,需要先查询数据库中的商品库存,确保逻辑正确,在本项目中,我们将库存信息信息存储在redis中,从而可以减少对数据库的访问。

秒杀接口优化思路:减少数据库的访问

  • 系统初始化时,将商品库存信息加载到redis中;
  • 服务端收到请求后,redis预减库存,如果库存不足,则直接进入下一步;
  • 服务端将请求入队,立即返回向客户端返回排队中的信息,提高用户体验;
  • 服务端请求出队,生成秒杀订单,减少库存;
  • 客户端轮询是否秒杀成功。

内存标记减少redis访问

实际上,访问redis也是有网络开销的,所以,在本项目中使用内存标记减少对redis的访问。

具体实现为:

  • edu.uestc.controller.MiaoshaController定义了一个HashMap<Long, Boolean>,在某一商品已经秒杀结束时,在HashMap中对该商品的库存进行标记,如果库存为0,则将该商品标记为已经秒杀结束。然后在从redis中读取库存信息前做一次判断,如果该商品已经秒杀结束,则不用在访问redis中的库存信息。

请求先入队缓冲,异步下单,增强用户体验

服务器收到秒杀请求后,不是直接访问数据库,而是将请求放置在队列(RabbitMQ)中,这样可以提高用户体验。然后秒杀请求出队,服务器生成秒杀订单,减少缓存中的库存,这就是异步下单的过程。

具体的实现过程为:

  • 如果秒杀商品库存尚有,则生成一条秒杀消息发送到消息队列中(信息中含有用户信息与商品id);
  • 消息的消费者收到秒杀消息后,从数据库中读取用户是否已经完成秒杀,如果没有,则减库存,下订单,写入订单信息到数据库中。

Nginx水平扩展

  • [ ] 待完成

压测

  • [ ] 待完成

超卖问题

超卖问题实际上是两个问题:

  1. 商品的库存减为负数,也就出现了超卖问题,这是不合理的;
  2. 同一个用户秒杀到了两个一样的商品,这种情形也是超卖,应当避免。

来看看两个问题出现的情形:

对于第一个问题,我们知道,秒杀需要执行两个关键的操作,第一个是从数据库减库存,第二个是生成订到插入到数据库,这两个操作共同构成了秒杀操作,因此,秒杀操作是一个事务

@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
    // 1. 减库存
    boolean success = goodsService.reduceStock(goods);
    if (!success) {
        setGoodsOver(goods.getId());
        return null;
    }
    // 2. 生成订单;向order_info表和maiosha_order表中写入订单信息
    return orderService.createOrder(user, goods);
}

如果商品是由10个,而达到goodsService.reduceStock()的请求有100个,当它们同时执行减库存操作时,会导致库存变为-90,这就引发了超卖问题。如何解决呢?

来看看第减库存操作的数据库的Mapper

@Update("UPDATE miaosha_goods SET stock_count = stock_count-1 WHERE goods_id=#{goodsId}")
int reduceStack(MiaoshaGoods miaoshaGoods);

因为每次UPDATE对于数据库来说都是原子的,如果每次减库存操作之前先判断库存是否大于零,则可以利用数据库层面的原子性来保证库存不会为负数,这也就解决了超卖的问题。

@Update("UPDATE miaosha_goods SET stock_count = stock_count-1 WHERE goods_id=#{goodsId} AND stock_count > 0")
int reduceStack(MiaoshaGoods miaoshaGoods);

AND stock_count > 0即为数据库层面解决超卖的保证。

对于第二个问题,考虑一种情形,如果一个未秒杀成功的用户同时对一个商品发出两次秒杀请求,对于两次秒杀请求,服务器层面会判断用户的两次秒杀请求为合法请求,然后完成从数据库减库存和将订单插入到数据库的操作,显然,这是不合理的。因为一个用户只能秒杀一个商品,如果执行成功,则订单表中会出现两条条商品id和用户id相同的记录,一个商品的库存被同一个用户减了两次(也可能是多次),这就引发了超卖问题。

因此,为了解决这个问题,我们要充分利用事务的特性。从数据库减库存和将订单记录插入到数据库构成了事务,如果一个操作未执行成功,则事务会回滚。如果我们对miaosha_order中的user_idgoods_id字段创建一个联合唯一索引,则在插入两条user_idgoods_id相同的记录时,将会操作失败,从而事务回滚,秒杀不成功,这就解决了同一个用户发起对一个商品同时发起多次请求引发的超卖问题。

至此,通过上述的分析,超卖问题就可以得到解决了。总结起来如下。

思路总结:

  • SQL加库存数量的判断:防止库存变为负数;

  • 数据库加唯一索引:防止用户重复购买。

安全优化

安全优化的手段

  • 隐藏接口地址
  • 数学公式验证码
  • 接口限流防刷

秒杀接口地址隐藏

为什么要做秒杀接口地址的隐藏?

首先看客户端秒杀执行的逻辑:

<button type="button" id="buyButton" onclick="doMiaosha()">立即秒杀</button>
function doMiaosha() {
    $.ajax({
        url: "/miaosha/do_miaosha_static",
        type: "POST",
        data: {
            goodsId: $("#goodsId").val(),
        },
        success: function (data) {
            if (data.code == 0) {
                getMiaoshaResult($("#goodsId").val());
            } else {
                layer.msg(data.msg);
            }
        },
        error: function () {
            layer.msg("客户端请求有误");
        }
    });
}

用户点击秒杀按钮后,会向服务端请求秒杀商品的秒杀信息,客户端的POST请求是以明文的方式发送给服务器的,如果使用一种工具将POST请求体中的数据和请求的URL组合起来,构成一个完整的POST请求,然后不停地向服务器请求资源,则会给服务器带来很大的压力,同时,这样一种作弊的方式带来的用户体验也是极差的,这样一种设计缺陷会被别有用心的人用于不正当交易,因此,需要一种方式克服这种缺陷,这就引出了秒杀接口的隐藏。

怎么做秒杀接口地址的隐藏

在秒杀开始之前,秒杀接口地址不要写到客户端,而是在秒杀开始之后,将秒杀地址动态地在客户端和服务器间进行交互完成拼接。这样一来,秒杀开始之前,秒杀地址对客户端不可见。

实现思路:

  • 秒杀开始之前,先去请求接口获取秒杀地址;
  • 接口改造,带上@pathVariable参数;
  • 添加生成地址的接口;
  • 秒杀收到请求,先验证@pathVariable参数。

用户在提交获取秒杀地址的请求之前,需要将goodsIdverifyCode一同提交到服务端,服务器通过@RequestParam参数获取goodsIdverifyCode,然后检验验证码是否正确,如果正确,则返回秒杀地址给客户端,客户端得到秒杀地址后,拼接秒杀地址然后异步地向这个地址发出请求获取秒杀结果,这样就完成了秒杀接口地址的隐藏。

需要注意的是,这里需要将goodsIdverifyCode一同提交到服务端做校验,如果只提交goodsId,那么客户端仍然可以使用明文的方式获取随机生成的接口秒杀地址,但是,引入了verifyCode后,客户端需要将验证码也一起发送到服务端做验证,验证成功才返回随机生成的秒杀地址,不成功则返回非法请求,通过这样一种双重验证的方式,就可以方式用户使用不合理的手段参与秒杀,引入验证码有效地防止了这一点,因为验证码的输入需要用户真正参与进来。

数学公式验证码

这种方式主要是防止客户端通过明文地址+goodsId将秒杀请求不停地发送到服务端,同时,也有效的防止机器人等手段参与秒杀。

验证码的作用:

  • 防止利用机器人等手段防止非目标用户参与秒杀;
  • 减少单位时间内的请求数量。对于一个秒杀商品,在开始秒杀后肯定会有许多用户参与秒杀,那么在开始秒杀的时候,用户请求数量是巨大,从而对服务器产生较大的压力,而通过验证码的方式就可以有效地将集中式的请求分散,从而达到削减请求峰值的目的。

实现思路:

在服务端计算出验证码的表达式的值,存储在服务端,客户端输入验证码的表达式值,传入服务端进行验证。

  • 点击秒杀之前,向让用户输入验证码,分散用户的请求;
  • 添加生成验证码的接口;
  • 在获取秒杀路径的时候,验证验证码;
  • ScriptEngine的使用(用于计算验证码上的表达式)。

当秒杀未开始时,商品详情页异步地向服务端发出获取商品详细信息的请求,同时,获取验证码。服务端收到获取验证码的请求后,生成验证码返回给客户端,同时,将验证码的结果存储再redis中,以便客户端发起秒杀请求时做验证码的校验。

接口限流防刷

有了对用户的访问进行一定的限制,就可以减轻服务器压力。例如通过访问次数的限制就是一种限流防刷的手段。即限制用户下一定的时间间隔内对接口的访问次数。

实现思路:对接口限流

一般来讲如果使用计时器来做这个功能,实现起来比较复杂。在这里,我们可以充分利用redis中的key-value过期机制来完成。

在redis中存储一个用于记录访问次数的变量,在过期时间内被继续访问,则次数变量加1,如果在过期时间内访问次数超出限制,则返回“频繁提交提示用户”。过期时间到了之后,将该变量删除。

因为可能需要对很对接口对限流防刷操作,如果对每一个接口都实现一遍限流防刷,则会导致代码过度冗余,因此,可以定义一个方法拦截器@AccessInterceptor拦截用户对接口的请求,统一对拦截限流逻辑处理,这样可以有效地减少代码的冗余。针对需要拦截请求的接口,添加注解@AccessLimit即可。

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].