简单商城Shop


简单商城Shop

系统总体架构

img

系统流程图

img

页面展示

imgimgimg

imgimgimg

系统工程

工程结构截图

img

Shop-Parent工程

存放其他工程的工程依赖,是其他工程的负工程,方便对依赖的管理而建立。

maven依赖如下:

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>1.5.2.RELEASE</version>
</parent>
<properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  <java.version>1.8</java.version>
</properties>
<dependencies>
  <!-- eureka server -->
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka-server</artifactId>
  </dependency>
  <!-- feign -->
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
  </dependency>
  <!-- 集成web -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <!-- 集成lombok 框架 -->
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
  </dependency>
  <!-- 集成redis -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
  <!-- 集成mysql -->
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.46</version>
  </dependency>
  <!-- freemarker -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
  </dependency>
  <!-- 阿里巴巴数据源 -->
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.14</version>
  </dependency>
  <!-- fastjson -->
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.30</version>
  </dependency>
  <!-- springboot整合activemq -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-activemq</artifactId>
  </dependency>
  <!-- 集成发送邮件 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
  </dependency>
</dependencies>

Shop-api工程

是SpringCloud远程调用的消费者和提供者的公共接口工程。包含Shop-api-item工程和Shop-api-member工程。

img

img

数据库设计

(1)用户表shop_users

CREATE TABLE `shop_user`(

`id` int(20) NOT NUll auto_increment COMMENT'主键(自增长)',

`username` VARCHAR(50) NOT NULL COMMENT'用户名',

`password` VARCHAR(32) NOT NULL COMMENT'密码,加密存储',

`phone` VARCHAR(20) DEFAULT NULL COMMENT'手机号',

`email` VARCHAR(50) DEFAULT NULL COMMENT'邮箱',

`openId` VARCHAR(100) DEFAULT NULL COMMENT'登录Id',

`created` TIMESTAMP not NULL DEFAULT CURRENT_TIMESTAMP COMMENT'自动插入,创建时间',

`updated` TIMESTAMP not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT'自动插入,修改时间',

PRIMARY KEY(`id`),

UNIQUE KEY `username` (`username`) USING BTREE,

UNIQUE KEY `phone` (`phone`) USING BTREE,

UNIQUE KEY `email` (`email`) USING BTREE

)ENGINE=INNODB COMMENT='用户表';

(2)商品信息表shop-item

CREATE TABLE `shop_item` (

 `id` bigint(20) NOT NULL auto_increment COMMENT '商品id,同时也是商品编号',

 `title` varchar(100) NOT NULL COMMENT '商品标题',

 `sell_point` varchar(500) DEFAULT NULL COMMENT '商品卖点',

 `price` bigint(20) NOT NULL COMMENT '商品价格,单位为:分',

 `num` int(10) NOT NULL COMMENT '库存数量',

 `barcode` varchar(30) DEFAULT NULL COMMENT '商品条形码',

 `image` varchar(500) not NULL COMMENT '商品图片',

 `desc_id` bigint(10) NOT NULL COMMENT '商品详情Id',

 `cid` bigint(10) NOT NULL COMMENT '所属栏目',

 `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '商品状态,1-正常,2-下架,3-删除',

`created` TIMESTAMP not NULL DEFAULT CURRENT_TIMESTAMP COMMENT'自动插入,创建时间',

`updated` TIMESTAMP not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT'自动插入,修改时间',

 PRIMARY KEY (`id`),

 KEY `cid` (`cid`),

 KEY `status` (`status`),

 KEY `updated` (`updated`)

) ENGINE=InnoDB COMMENT='商品表';

(3)商品描述表shop_item_desc

CREATE TABLE `shop_item_desc` (

 `id` bigint(20) NOT NULL auto_increment COMMENT '商品ID',

 `itemdesc` text COMMENT '商品描述',

`created` TIMESTAMP not NULL DEFAULT CURRENT_TIMESTAMP COMMENT'自动插入,创建时间',

`updated` TIMESTAMP not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT'自动插入,修改时间',

 PRIMARY KEY (`id`)

) ENGINE=InnoDB COMMENT='商品描述表';

(4)商品类目表shop_item_category

CREATE TABLE `shop_item_category` (

 `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '类目ID',

 `name` varchar(50) not NULL COMMENT '类目名称',

 `img` varchar(150) not NULL COMMENT '图片地址',

 `status` int(1) DEFAULT '1' COMMENT '状态。可选值:1(正常),2(删除)',

`created` TIMESTAMP not NULL DEFAULT CURRENT_TIMESTAMP COMMENT'自动插入,创建时间',

`updated` TIMESTAMP not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT'自动插入,修改时间',

 PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='商品类目';

实体类设计

完全和数据库字段一致。

Shop-common工程

将工程中常用的功能方法和类抽离出来。

img

Shop-EurekaServer工程

系统的Eureka注册中心工程。

img

配置文件:

server:
 port: 8761
 context-path: /
eureka:
 instance:
   hostname: localhost
 client:
   registerWithEureka: false
   fetchRegistry: false
   serviceUrl:
    defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

Shop-Item工程

商品工程。

img

(1)额外添加的maven依赖

<!-- 集成mybatis -->
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>1.1.1</version>
</dependency>

(2)配置文件

server:
 port: 8764
 context-path: /item
eureka:
 client:
   serviceUrl:
    defaultZone: http://localhost:8761/eureka/
mybatis:
 configuration:
# 开启驼峰uName自动映射到u_name
   map-underscore-to-camel-case: true
spring:
 application:
   name: item
 redis:
   host: localhost
   password: 123457
   port: 6739
   pool:
    max-idle: 100
    min-idle: 1
    max-active: 1000
    max-wait: -1
 datasource:
   name: test
   url: jdbc:mysql://localhost:3306/shop
   username: root
   password: root
# 使用druid数据源
   type: com.alibaba.druid.pool.DruidDataSource
   driver-class-name: com.mysql.jdbc.Driver
   filters: stat
   maxActive: 20
   initialSize: 1
   maxWait: 60000
   minIdle: 1
   timeBetweenEvictionRunsMillis: 60000
   minEvictableIdleTimeMillis: 300000
   validationQuery: select 'x'
   testWhileIdle: true
   testOnBorrow: false
   testOnReturn: false
   poolPreparedStatements: true
   maxOpenPreparedStatements: 20

Shop-Member工程

用户工程。

img

(1)额外添加的依赖

<!-- 集成mybatis -->
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>1.1.1</version>
</dependency>

(2)配置文件

server:
 port: 8762
 context-path: /member
eureka:
 client:
   serviceUrl:
    defaultZone: http://localhost:8761/eureka/
spring:
 application:
   name: member
 redis:
   host: localhost
   password: 123457
   port: 6379
   pool:
    max-idle: 100
    min-idle: 1
    max-active: 1000
    max-wait: -11
 datasource:
    name: test
    url: jdbc:mysql://localhost:3306/shop
    username: root
    password: root
    # 使用druid数据源
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    filters: stat
    maxActive: 20
    initialSize: 1
    maxWait: 60000
    minIdle: 1
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: select 'x'
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    maxOpenPreparedStatements: 20
 activemq:
   broker-url: tcp://localhost:61616
   user: admin
   password: admin
message:
	queue: message_queue

Shop-Message工程

发送注册邮件工程。

img

邮件的自定义报文:

{
   "header":{
     "interfaceType":"接口类型"
   },
   "content":{
     "mail":" ",
     "userName":" "
  }
 }

配置文件:

server:
 port: 8763
 context-path: /message
eureka:
 client:
   serviceUrl:
    defaultZone: http://localhost:8761/eureka/
spring:
 application:
   name: message
 activemq:
   broker-url: tcp://localhost:61616
   user: admin
   password: admin
 mail:
   host: smtp.163.com
   username: xiaoqianittest@163.com
   password: DGTQZCESAMGOWPVT
   enable: true
   smtp:
    auth: true
    starttls:
     enable: true
     required: true

Shop-Moblie-Web工程

用户交互工程。

img img

Redis中购物车信息格式:

key: userOpenId 

value: { "cart":["itemId1", "itemId2", "itemId3"...] }

(1)额外添加依赖

<!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java -->
<!-- 整合支付宝网关接口 -->
<dependency>
  <groupId>com.alipay.sdk</groupId>
  <artifactId>alipay-sdk-java</artifactId>
  <version>3.7.110.ALL</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<!-- 整合jsp -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<dependency>
  <groupId>org.apache.tomcat.embed</groupId>
  <artifactId>tomcat-embed-jasper</artifactId>
</dependency>

(2)配置文件

server:
# port: 8763
 port: 80
 context-path: /Shop
eureka:
 client:
   serviceUrl:
    defaultZone: http://localhost:8761/eureka/
spring:
 freemarker:
   suffix: .ftl
   templateEncoding: UTF-8
   templateLoaderPath: classpath:/templates/
 mvc:
   view:
    prefix: /WEB-INF/
    suffix: .jsp
 application:
   name: shop
 redis:
   host: localhost
   password: 123457
   port: 6379
   pool:
    max-idle: 100
    min-idle: 1
    max-active: 1000
		max-wait: -1

系统介绍

本系统是在线购物支付网站,采用SpringCloud框架进行分布式系统的设计。整个系统中的角色包含注册中心,服务提供者和服务消费者。这里注册中心使用Spring Cloud Eureka,服务消费者是与用户交互的Web工程。服务提供者,共包含3个服务工程:用户服务、商品服务和邮件服务。服务的调用使用Spring Cloud Feign,RPC远程调用的数据通信是Restful风格,使用Json串。系统的数据库采用mySql,ORM框架使用Mybatis。

用户服务包含用户登录、注册和注册邮件通知功能。对应的数据库表包含用户名、邮件、密码等字段,其中用户名、邮件和手机号加了唯一索引。用户注册时,使用MD5算法对密码加密后再存入数据库。入数据库成功,则发送邮件报文到ActiveMQ的消息队列,等待邮件服务监听消息并发送邮件。用户登录在验证用户名和密码后,将随机生成的字符串作为loginToken令牌作为键,将用户的openId作为值存储到Redis中,目的是为了后面的购物车相关操作。

商品服务包含首页展示商品、查询商品详情功能。对应的数据库表分为商品信息表、商品详情表、商品类目表三个表。目的是为了解耦商品的信息,对商品表进行垂直拆分,表之间通过特定字段进行关联。首页展示商品时,将查询到的信息按照类目进行分类,最后封装到Map中传到前端页面。查询商品详情时,根据用户点击前端页面传回的商品Id值进行查询商品详情描述,并返回前端页面即可。

邮件服务包含监听邮件消息和发送邮件两部分。使用JMSListener监听队列消息,有消息时对消息格式进行校验,邮件格式为自定义报文,有特定的报文头。监听和校验完又叫消息后,对报文解析后发送邮件即可。

最后建立一个Web工程,作为服务消费,用来消费上述三个服务,同时提供和用户交互的页面和业务逻辑处理。除调用上述三个服务进行用户登录、注册、商品展示和查询功能外,该工程单独提供了购物车和支付功能。购物车功能以Redis服务器为核心进行存储信息。按照自定义的购物车信息格式,为两层键值对形式,第一层键为用户的openId,第二层键为常量字符串”cart”,值为商品id的json数组。由此可以进行购物车的展示、删除商品、新增商品功能。最后支付功能对接支付宝的支付网关接口。使用支付宝开放平台的沙箱账号和工具以及Demo进行支付功能的整合。

系统的启动,只需启动Eureka、服务提供者和消费者工程即可。

一些开发笔记

我踩过的坑:

1、通过@requestBody可以将请求体中的JSON字符串绑定到相应的bean上,当然,也可以将其分别绑定到对应的字符串上。

2、在feignClient(服务提供者)端,要注意暴露接口的返回值为JSON,可以用@Controller + @ResponsedBody或者@RestController修饰。否则报错feign.FeignException: status 404 reading

3、在html中,href中,不加/包含context-path,如href=”index”,如果加/代表从域名开始拼接,href=”/mobile/index”

4、从前端传过来的参数,若为空,是空字符串””,而不是null。

5、fastjson,注意怎么传list<类>,特别注意JSONARRAY里面的类型好像只能是Integer,注意怎么转化为需要的JAVA基本类型

6、Mybatis的Maven依赖,如果引用了,但是没有配置mysql会在启动报错

7、loadbalance error …client: item,这类错误,可能需要重启一下工程即可

8、从json中提取实体类:ItemEntity item = itemJson.getObject(Constants.HTTP_RES_CODE_DATA, ItemEntity.class)

9、springcloud的api接口的参数需要用@RequestParam

关于用户登录信息验证

​ 我这里的设计为:cookie验证登陆+redis存储用户信息/状态(openid)。用户登录时生成随机令牌token,写cookie(“token”, token),在写redis(token, openid)。用户再次登陆时cookie中携带token,即可验证状态。

​ 如果客户端禁用cookie,这里可以设置session重写。用户登陆后写session(“token”, openid),后将对应的JSSESSIONID写入页面的隐藏域中。下次登陆时页面填入JSSESSIONID发送给客户端,登陆后即可验证登陆状态。

电商系统/秒杀系统常见问题

问题概览

  • 库存预热

    提前把商品的库存加载到Redis中去,让整个流程都在Redis里面去做,然后等秒杀结束了,再异步的去修改库存就好了。

  • 用户下单

    为了提升下单速度,我们将订单数据存入到redis缓存中,如果用户支付了,再将redis中的订单存到Mysql中,并清空redis中的订单。如果没有库存了,则将缓存中的商品数据同步到Mysql中,并情况redis中该商品的缓存。

  • 创建订单

    用户每次下单的时候,我们可以创建一个队列进行排队,然后采用异步的方式创建订单,排队我们可以采用Redis的队列实现。如果符合下单资格,只需要先记录用户下单数据,存入redis队列(先不要创建订单),然后采用异步创建订单的方式,从redis队列中,读取下单信息,创建订单。

    redis队列的实现

  • 下单状态查询

    我们需要做一个页面判断,每过1s查询一次下单状态。

  • 防止重复排队

    我们用一个保存排队信息的hash键值对。用户每次抢单的时候,一旦排队,我们设置一个自增值,让该值的初始值为1,只有当该值为1的时候才能进行后续下单操作。如果值>1,则表明已经排队,不允许重复排队,如果重复排队,则对外抛出异常,并抛出异常信息100表示已经正在排队。

  • 超卖问题

    判断商品数量的时候,不要用传统的读取操作,而是用redis的增量命令,才能确保数据的精准性。

    redis的增量命令

  • 订单支付

    下单成功后会跳转到支付页面,支付页面会根据用户名查看用户订单,创建预支付信息,发送请求到支付中心,获取二维码。支付状态通过回调地址发送给MQ,我们需要侦听这个MQ,如果支付成功,则修改订单状态,将订单信息写入mysql,清除redis中的订单信息和排队信息等。如果支付失败,则删除订单,回滚库存。

  • 超时支付订单库存回滚

    如果一段时间未支付,则需要回滚库存,删除订单操作。采用rabbitMQ的延时消息队列来实现。
    Rabbitmq实现延时队列一般而言有两种形式:

    - 第一种方式:利用两个特性: Time To Live(TTL)、Dead Letter Exchanges(DLX)[A队列过期->转发给B队列]
    - 第二种方式:利用rabbitmq中的插件x-delay-message
    TTL
    RabbitMQ可以针对队列设置x-expires(则队列中所有的消息都有相同的过期时间)或者针对Message设置x-message-ttl(对消息进行单独设置,每条消息TTL可以不同),来控制消息的生存时间,如果超时(两者同时设置以最先到期的时间为准),则消息变为dead letter(死信)
    Dead Letter Exchanges(DLX)
    RabbitMQ的Queue可以配置x-dead-letter-exchange和x-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter,则按照这两个参数重新路由转发到指定的队列。
    x-dead-letter-exchange:出现dead letter之后将dead letter重新发送到指定exchange
    x-dead-letter-routing-key:出现dead letter之后将dead letter重新按照指定的routing-key发送
    

超卖问题

总结:

  • SQL层面:where条件
  • 服务端代码层面:乐观锁
  • 架构层面:队列 | Redis减库存
  1. 参考高并发下怎样优雅的保证扣减库存数据的正确性

    • 如果扣减多个库存num时
      update coupon set stock=stock - #{num} where id = #{couponId} and stock >= #{num}

    • 解释场景:

      使用业务自身的条件做为乐观锁,但是存在ABA问题,对比其他方案的好处是不用增加version版本字段。

      id是主键索引的前提下,采用上面的方式,只做数据安全校验,可以有效减库存,性能更高,避免大量无用sql,只要有库存就也可以操作成功.

  2. 参考秒杀系统如何防止超卖?

    为了解决上面的超卖问题,我们当然可以在Service层给更新表添加一个事务,这样每个线程更新请求的时候都会先去锁表的这一行(悲观锁),更新完库存后再释放锁。可这样就太慢了,1000个线程可等不及。

    所以我们需要乐观锁。一个最简单的办法就是,给每个商品库存一个版本号version字段

    CREATE TABLE `stock` (
      `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
      `name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称',
      `count` int(11) NOT NULL COMMENT '库存',
      `sale` int(11) NOT NULL COMMENT '已售',
      `version` int(11) NOT NULL COMMENT '乐观锁,版本号',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    我们在实际减库存的SQL操作中,首先判断version是否是我们查询库存时候的version,如果是,扣减库存,成功抢购。如果发现version变了,则不更新数据库,返回抢购失败。

    <update id="updateByOptimistic" parameterType="cn.monitor4all.miaoshadao.dao.Stock">
        update stock
        <set>
          sale = sale + 1,
          version = version + 1,
        </set>
        WHERE id = #{id,jdbcType=INTEGER}
        AND version = #{version,jdbcType=INTEGER}
      </update>
  3. 参考解决秒杀系统超卖问题的三种方案

    • 引入队列,然后将所有写DB操作在单队列中排队,完全串行处理。当达到库存阀值的时候就不在消费队列,并关闭购买功能。这就解决了超卖问题。
      优点:解决超卖问题,略微提升性能。
      缺点:性能受限于队列处理机处理性能和DB的写入性能中最短的那个,另外多商品同时抢购的时候需要准备多条队列。

    • 将写操作前移到Memcached中,同时利用Memcached的轻量级的锁机制CAS来实现减库存操作。
      优点:读写在内存中,操作性能快,引入轻量级锁之后可以保证同一时刻只有一个写入成功,解决减库存问题。
      缺点:没有实测,基于CAS的特性不知道高并发下是否会出现大量更新失败?不过加锁之后肯定对并发性能会有影响。

    • 将提交操作变成两段式,先申请后确认。然后利用Redis的原子自增操作(相比较MySQL的自增来说没有空洞),同时利用Redis的事务特性来发号,保证拿到小于等于库存阀值的号的人都可以成功提交订单。然后数据异步更新到DB中。
      优点:解决超卖问题,库存读写都在内存中,故同时解决性能问题。
      缺点:由于异步写入DB,可能存在数据不一致。另可能存在少买,也就是如果拿到号的人不真正下订单,可能库存减为0,但是订单数并没有达到库存阀值。

  4. 常见减库存的方式

    电商场景下的购买过程一般分为两步:下单和付款。“提交订单”即为下单,“支付订单”即为付款。基于此设定,减库存一般有以下几个方式:

    • 下单减库存。买家下单后,扣减商品库存。下单减库存是最简单的减库存方式,也是控制最为精确的一种
    • 付款减库存。买家下单后,并不立即扣减库存,而是等到付款后才真正扣减库存。但因为付款时才减库存,如果并发比较高,可能出现买家下单后付不了款的情况,因为商品已经被其他人买走了
    • 预扣库存。这种方式相对复杂一些,买家下单后,库存为其保留一定的时间(如 15 分钟),超过这段时间,库存自动释放,释放后其他买家可以购买

    能够看到,减库存方式是基于购物过程的多阶段进行划分的,但无论是在下单阶段还是付款阶段,都会存在一些问题。

    预扣库存是目前比较流行的方式。

支付的幂等性

定义

  1. 前端重复提交选中的数据,后台应该只产生对应本次提交的一个响应结果。

  2. 用户发起一笔付款请求,应该只扣除用户账号一次钱,即使遇到网络重发或系统bug重发时,也只扣除一次钱。

  3. 创建业务订单时,一次业务请求只能创建一个订单

实现幂等

需要有唯一标识来标记请求,比如订单号,token等。

  1. 先查询一下订单是否已经支付过;

  2. 如果已经支付过,则返回支付成功;如果没有支付,进行支付流程,修改订单状态为‘已支付’。 保证幂等性就需要查询和变更状态操作加锁,将并行操作改为串行操作。

懒删除

​ 实战项目中对记录的删除,是调整某字段值,比如1为显示,-1为不显示。


文章作者: 小小千千
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小小千千 !
评论
  目录