1,项目介绍

1.1,问题难点

秒杀活动会让用户在限定时间内以较低的价格来抢购商品,此时就极容易引起瞬时的高并发请求。技术难点包括:基于Redis的分布式session、基于Redis的数据缓存、基于Redis的页面缓存、基于RabbitMQ的秒杀限流。

【项目难点】

  • 限流、削峰部分的设计:例如有10W用户来抢购10件商品,我们只放100个用户进来。采取发放令牌机制(控制流量),根据商品id和一串uuid产生一个令牌存入redis中同时引入了秒杀大闸,目的是流量控制,比如当前活动商品只有100件,我们就发放500个令牌,秒杀前会先发放令牌,令牌发放完则把后来的用户挡在这一层之外,控制了流量。
  • 分布式Session:做完了分布式扩展之后,发现有时候已经登录过了但是系统仍然会提示去登录,后来经过查资料发现是cookie和session的问题。然后通过设置cookie跨域分享以及利用redis存储token信息得以解决。
  • 基于RabbitMQ的削峰处理:当秒杀请求到来时,控制器的处理方法接收到该秒杀请求后,该控制器并不直接调用Service组件的方法来处理秒杀请求,而是简单地发送一条消息到RabbitMQ消息队列。
  • 高并发:在秒杀活动中,可能需要处理数十万到数百万的请求,而且这些请求通常都是集中在短时间内到达的。通过Jmeter压力测试,系统的QPS从150/s 提升到2000/s ,QPS = (1000 毫秒 / 响应时间) * 机器数目。

  • 数据库优化:由于秒杀活动中存在大量的读写请求,所以需要对数据库进行优化,包括缓存、分库分表、读写分离等。

  • 分布式部署:为了提高系统的可用性和性能,可以考虑将系统部署在多台服务器上,实现分布式架构。

  • 第三方集成问题:涉及到很多第三方服务的集成,如支付、短信、物流等。我通过查阅文档、调试接口等方式来解决第三方集成问题,并且编写了相应的异常处理机制来保证系统的稳定性。

1.2,Redis场景问题

【问题】加入购物车是否与减库存?

【解决】是的,当用户将商品添加到购物车时,通常会将商品的库存数量减少。这是因为库存数量表示可供销售的商品数量,而一旦用户将商品添加到购物车中,该商品将被视为已售出或已预订,因此库存数量必须相应减少。

  • 并发操作:多个用户同时尝试将同一商品添加到购物车中,此时需要确保系统能够正确地处理库存数量的更新,避免出现库存不足或超卖等问题。

  • 超时操作:当用户将商品添加到购物车中后,但在一定时间内未完成购买操作,此时系统需要将该商品重新放回库存中,以确保其他用户仍然能够购买该商品。

  • 取消订单:如果用户取消了订单,或者在一定时间内未完成订单支付,此时需要将商品重新放回。

【问题】10w人抢100件商品,Redis单线程瓶颈如何解决?

【解决】

  • 使用Redis集群:Redis集群可以横向扩展,将数据分散到多个节点上,从而提高并发处理能力。因此,可以考虑将Redis部署为集群,以提高处理并发请求的能力。

  • 使用Redis缓存穿透技术:缓存穿透是指查询缓存中不存在的数据,导致每次请求都需要访问数据库。为了避免这种情况的发生,可以采用布隆过滤器或者其他缓存穿透技术,将不可能存在的数据过滤掉,从而减少不必要的数据库访问,降低单线程的压力。

  • 使用Redis Lua脚本:Redis支持Lua脚本,可以编写一些复杂的操作,比如加锁、解锁、扣减库存等。

  • 使用分布式锁:在高并发场景下,多个用户同时操作同一份数据,会导致数据不一致问题。因此,可以使用分布式锁技术,保证同一时刻只有一个用户能够对数据进行修改,从而避免数据冲突问题。

  • 使用Redis Pipeline技术:Redis Pipeline可以将多个请求合并成一个批处理请求,减少单次请求的网络开销和响应时间。

【问题】Redis宕机怎么办?

  • 首先,Redis 支持主从复制(replication)模式,即将一个 Redis 服务器(即主节点)的数据复制到多个 Redis 服务器(即从节点)中,从而提高系统的可用性和可靠性。当主节点出现宕机时,从节点可以接替主节点的工作,保证系统的正常运行。
  • 此外,Redis 还支持 Sentinel(哨兵)模式,Sentinel 会监控主节点的健康状态,并在主节点宕机时自动将从节点升级为主节点,从而实现 Redis 的自动故障切换。
  • 其次,Redis 还支持持久化(persistence)功能,可以将数据保存到磁盘上,防止数据丢失。Redis 提供两种持久化方式:RDB 和 AOF。RDB 是一种快照方式,可以定期将内存中的数据保存到磁盘中,并生成一个 RDB 文件;AOF(Append Only File)则是一种日志方式,可以将每个写操作记录到日志文件中,以便在 Redis 重启时进行恢复。通过使用持久化功能,即使 Redis 出现宕机等异常情况,也可以通过重新加载磁盘中的数据进行恢复。
  • 另外,Redis 还提供了 Cluster 集群模式,该模式将多个 Redis 节点组成一个集群,实现了数据的分片和自动故障转移功能,从而提高了 Redis 的可用性和可靠性。当某个节点宕机时,Redis 会自动将该节点的数据迁移到其他节点上,从而保证系统的正常运行。

【问题】Redis内存碎片?

【解决】内存碎片指的是在内存池中分配的内存块被释放后,留下了无法使用的小块内存。这些小块内存加起来可能会占用相当大的空间,但由于它们太小而无法重新使用,因此它们实际上浪费了内存。在Redis中,内存碎片可能会影响到性能。当Redis需要分配大块内存时,如果没有足够的连续空闲内存可用,Redis将被迫执行内存碎片整理操作,这将导致Redis暂停处理客户端请求,直到操作完成。

  • 避免频繁地执行Redis命令,这样可以减少内存碎片的产生。

  • 配置Redis内存池的大小,确保足够的内存可用,从而减少内存碎片的产生。

  • 定期重新启动Redis进程,这将释放内存并清除内存碎片。

  • 如果必须使用大量短暂的内存块,可以使用Redis的jemalloc分配器,它可以有效地处理内存碎片。

1.3,削峰限流

【问题】秒杀系统面临的问题有哪些?

【解决】高并发;超卖、重复卖问题;脚本恶意请求;数据库扛不住;加了缓存之后的缓存三大问题(击穿、穿透、雪崩)。

【问题】做了什么限流削峰的措施?

【解决】

  • 在Nginx层添加限流模块限制平均访问速度。
  • 通过设置数据库连接池、线程池的最大线程数来限制总的并发数。
  • 通过Guava提供的Ratelimiter限制接门的访问速度。
  • TCP通信协议中的流量整形。
  • 验证码做防刷功能。
  • 于RabbitMQ的秒杀限流。

【问题】一个人同时用电脑和手机去抢购商品,会颁发几个token?

【解决】首先多台设备登录属于SSO问题,用户登录一端之后另外一端可以通过扫码等形式登录。虽然用户登录了多台设备,但是用户名是一样的。为用户办法的token是相同的。我们为一个用户只会颁发一个token。

【问题】介绍一下一致性哈希

【解决】一致性哈希算法它能够在节点的增加或删除时最小化数据迁移量,同时保持较好的负载均衡。该算法的基本思想是将节点和数据映射到一个固定的哈希环上,通过哈希值来确定数据应该路由到哪个节点。

  • 构建哈希环:将所有的节点和数据映射到一个固定范围的哈希环上,通常使用哈希函数(如MD5、SHA-1等)将节点或数据的标识映射为一个哈希值。

  • 添加节点:将所有的节点按照哈希值的顺时针方向放置在哈希环上。可以根据节点的标识(如IP地址、主机名等)计算哈希值,并将节点放置在离该哈希值最近的位置上。

  • 数据路由:当有新的数据到达时,根据数据的标识计算哈希值,并在哈希环上找到离该哈希值最近的节点。数据将被路由到该节点上进行处理。

  • 节点故障处理:当一个节点故障或被移除时,只需要重新映射该节点的数据到离该节点顺时针方向最近的节点上。这样可以最小化数据迁移量,保持负载均衡。

一致性哈希算法的优点是在节点的增加或删除时,只有少部分数据需要重新映射,大部分数据仍然保持在原来的节点上,从而减少了数据迁移的成本和影响。

import java.util.*;

public class ConsistentHashing {
    private final TreeMap<Integer, String> circle = new TreeMap<>();
    private final int numberOfReplicas;

    public ConsistentHashing(int numberOfReplicas) {
        this.numberOfReplicas = numberOfReplicas;
    }

    public void addNode(String node) {
        for (int i = 0; i < numberOfReplicas; i++) {
            String virtualNode = node + "#" + i;
            int hash = getHash(virtualNode);
            circle.put(hash, node);
        }
    }

    public void removeNode(String node) {
        for (int i = 0; i < numberOfReplicas; i++) {
            String virtualNode = node + "#" + i;
            int hash = getHash(virtualNode);
            circle.remove(hash);
        }
    }

    public String getNode(String key) {
        if (circle.isEmpty()) {
            return null;
        }
        int hash = getHash(key);
        Map.Entry<Integer, String> entry = circle.ceilingEntry(hash);
        if (entry == null) {
            entry = circle.firstEntry();
        }
        return entry.getValue();
    }

    private int getHash(String key) {
        // 使用简单的哈希函数,如MD5或SHA-1
        return key.hashCode();
    }
}

1.4,超卖超买 

【问题】如何解决超卖问题?

【解决1】利用redis的单线程特性预减库存处理秒杀超卖问题。在系统初始化时,将商品以及对应的库存数量预先加载到Redis缓存中(缓存预热);接收到秒杀请求时,在Redis中进行预减库存(decrement),当Redis中的库存不足时或者重复订单时,直接返回秒杀失败,否则继续进行第下一步;将请求放入RabbitMQ,返回正在排队中;RabbitMQ将请求出队,出队成功的请求可以生成秒杀订单,减少数据库库存,返回秒杀订单详情。

【解决2】直接由数据库操作库存的sql语句如下所示。依靠MySQL中的排他锁实现。

update table_prmo set num = num - 1 WHERE id = 1001 and num > 0

【解决3】在多线程环境下,使用锁来保护对同一个 key 的并发访问。例如,可以使用分布式锁来确保同时只有一个线程能够修改某个 key。

【解决3】Redis 支持事务操作,可以使用 MULTI 和 EXEC 命令来将多个操作打包成一个事务,在 EXEC 执行时才一起提交。这可以确保一组操作要么全部成功,要么全部失败,避免中间状态。

【问题】秒杀中如何解决重复下单问题?

【解决】mysql唯一索引(商品索引)+ 分布式锁

【问题】如何解决客户的恶意下单问题?

【解决】封IP,nginx中有一个设置,单个IP访问频率和次数多了之后有一个拉黑操作。

【问题】多机器扣减库存,如何保证它的线程安全的?

【解决】分布式锁。redission客户端实现分布式锁。

1.5,分布式锁,分布式事务

【问题】Redis和MySQL数据一致性如何保证?

(1)先删除缓存,再写入数据库。如果先删除Redis缓存数据,然而还没有来得及写入MySQL,另一个线程就来读取;这个时候发现缓存为空,则去Mysql数据库中读取旧数据写入缓存,此时缓存中为脏数据;然后数据库更新后发现Redis和Mysql出现了数据不一致的问题。
(2)先写入数据库,再删除缓存。如果先写了库,然后再删除缓存,不幸的写库的线程挂了,导致了缓存没有删除;这个时候就会直接读取旧缓存,最终也导致了数据不一致情况;因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。

(3)延时双删:在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。

public void write( String key, Object data ){
    //先删除缓存
    redis.delKey(key);
    //再写数据库
    db.updateData(data); 
    //休眠500毫秒
    Thread.sleep(500);
    //再次删除缓存
    redis.delKey(key);
}

【问题】这个500毫秒怎么确定的,具体该休眠多久时间呢?

  • 需要评估自己的项目的读数据业务逻辑的耗时。
  • 这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
  • 当然这种策略还要考虑redis和数据库主从同步的耗时。
  • 最后的的写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms即可。

【问题】方案缺点?

  • 在缓存过期时间内发生数据存在不一致。
  • 同时又增加了写请求的耗时。

(4)异步更新:利用Mysql binlog 进行增量订阅消费;将消息发送到消息队列;通过消息队列消费将增量数据更新到Redis上。其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

  • 读取Redis缓存:热数据都在Redis上;

  • 写Mysql:增删改都是在Mysql进行操作;

  • 更新Redis数据:Mysql的数据操作都记录到binlog,通过消息队列及时更新到Redis上。

【问题】Redis 怎么实现分布式锁?

【方式1】setnx(原子性)与set方法类似,同样具有保存数据的功能,且语法也为setnx (key,value,expire)。与set方法不同的是:执行setnx方法后,当不存在对应key时,返回1,表示设置成功。执行setnx方法后,当存在对应key时,返回0,表示设置失败。

  • 在Redis中使用setnx命令来尝试获取锁。该命令会在指定的键不存在时设置键值对,并返回1表示设置成功,返回0表示键已经存在,获取锁失败。

  • 如果获取锁成功,为了防止线程内部的死循环及网络问题,则设置一个过期时间,超过该时间,锁自动释放,以避免锁被长期持有。可以使用Redis的expire命令来设置过期时间。

  • 在使用完锁后,通过del命令删除键值对,释放锁。

【方式2】RedLock是Redis官方提出的一种分布式锁算法,基于多个独立的Redis节点实现,可用于提供高可用的分布式锁服务。

  • 客户端向N个Redis节点(通常为5个)请求获取锁,并设定相同的过期时间。

  • Redis节点以原子操作的方式执行以下三个步骤:(1)检查当前锁是否已经被其他客户端持有,如果没有则继续执行下一步;否则等待一段时间再次尝试获取锁。(2)使用当前时间戳和设置的过期时间计算锁的有效期限,并将锁的信息以一致性哈希的方式存储在 Redis 节点中。(3)将锁的信息返回给客户端。

  • 客户端在多数节点上成功获取锁后确认获取成功,否则认为获取失败。

  • 在锁的有效期限内,客户端需要定时向Redis节点发送请求,更新锁的信息以避免锁过期。

【方式3】ZooKeeper实现分布式锁排他的目的,只需要用到节点的特性:临时节点,以及同节点的唯一性。

  • 获得锁:所有用户可以去ZK服务器上/Exclusive_Locks节点下创建一个临时节点/lock。ZK基于同级节点的唯一性,会保证所有客户端中只有一个客户端能创建成功,创建成功的客户端获得了排他锁,没有获得锁的客户端需要通过Watcher机制监听/Exclusive_Locks节点下的子节点的变更事件,用于实时监听/lock节点的变化情况。当/lock节点被删除后,这些客户端收到通知,再次发起创建/lock节点的操作。
  • 释放锁:(1)获得锁的客户端因为异常断开和服务端为链接,基于临时节点的特性,/lock节点会被自动删除。(2)获得锁的客户端执行完业务逻辑之后,主动删除了创建的/lock节点。

【问题】RabbitMQ 怎么实现分布式事务?两阶段、TCC(Try-Confirm-Cancel)和消息事务。其中,消息事务是迄今为止实现分布式事务应用最多的解决方案。

【解决】

  • 使用 RabbitMQ 的事务机制:RabbitMQ 提供了事务机制,可以确保一组消息被原子性地提交或回滚。你可以通过开启 Channel 的事务模式来启用 RabbitMQ 的事务机制。当 Channel 处于事务模式时,所有的操作(包括发送和确认消息)都需要显式地提交或回滚。

  • 使用消息确认机制:RabbitMQ 提供了消息确认机制,可以确保消息已经被消费者成功处理。你可以通过开启消息确认模式来启用 RabbitMQ 的消息确认机制。当 Channel 处于消息确认模式时,消费者需要发送一个确认消息来告诉 RabbitMQ 消息已经被成功处理。

  • 使用消息回调机制:RabbitMQ 提供了消息回调机制,可以确保消息被正确地处理。你可以在生产者和消费者中分别注册回调函数,以确保消息被正确地处理。

  • 使用分布式事务协调器:如果你需要实现跨越多个 RabbitMQ 实例的分布式事务,你可以使用分布式事务协调器。一些流行的分布式事务协调器包括 Google 的 Chubby 和 Apache 的 ZooKeeper。

【问题】如果项目中的Redis挂掉,如何减轻数据库的压力?

【解决】redis集群;主从模式;哨兵模式;集群模式;

【问题】如果项目中的Mysql挂掉,如何解决?

【解决】Redis持久化;主从模式;将Redis降级为只读缓存;使用Redis的发布-订阅模式将Redis中的数据推送到其他系统中;

1.6,订单问题

【问题】如何去减Redis中的库存?

【解决】decrement API减库存,increment API回增库存。以上的指令都是原子性的。

【问题】减库存成功了,但是生成订单失败了,该怎办?

【解决】使用Spring提供的事务功能即可;分布式事务:将减库存与生成订单操作组合为一个事务。要么一起成功,要么一起失败。

【问题】订单取消怎么回滚?

【解决】如果订单被取消,您需要在数据库中删除该订单信息,并且需要回滚Redis中的库存。为了回滚Redis中的库存,您可以使用Redis的事务机制来实现。

  • 使用MULTI开启一个事务。
  • 然后使用INCRBY命令将扣减的库存数量加回去。
  • EXEC命令进行提交事务。

1.7,相关技术介绍

【Redis作用】

  • 管理分布式Session:基本原理是将session信息存储在Redis中,而不是存储在单个服务器的内存或硬盘中。这样,在多个服务器之间进行负载均衡时,用户的会话状态可以得到保留。具体实现可以采用Redis中的Hash数据类型,使用Session ID作为key,存储用户相关的信息(如用户ID、用户名等)作为field和value进行存储。当用户访问另一个服务器时,服务器从Redis中获取对应的session信息,以恢复用户的会话状态。
  • 缓存预热:本系统不仅会缓存数据库中那些需要频繁访问的数据;针对高并发场景进行了页面优化,缓存页面至浏览器,前后端分离降低服务器压力,加快用户访问速度。
  • 预减库存,防止超卖功能实现。
  • 分布式锁:多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!

【Redis热点数据】

  • 用户信息:Redis可以用于缓存用户信息,同时实现分布式Session。

  • 商品信息:将产品信息缓存在Redis中,可以降低数据库的IO负载,加快页面加载速度,并且更好地维护页面缓存。

  • 购物车:购物车是商城系统中非常重要的一个功能,通过将购物车信息存储在Redis中,可以快速响应用户的添加、删除商品等操作,并且减轻数据库的压力。此外,还可以在用户下单时对购物车数据进行清理,以及对未支付的订单进行处理。

  • 订单信息:商城系统中的订单是基于用户购买行为的,对于订单信息的查询和处理需要高效。

  • 历史记录:当用户进行搜索时,可以将其输入的关键词或者搜索条件存储到Redis中,以便下一次进行相同的搜索时可以直接从缓存中获取结果,提高搜索速度和用户体验。

【RabbitMQ作用】

  • 异步处理。
  • 引入了RabbitMQ消息组件对瞬时高并发请求进行限制,延迟处理,从而避免服务器崩溃:当秒杀请求到来时,控制器的处理方法接收到该秒杀请求后,该控制器并不直接调用Service组件的方法来处理秒杀请求,而是简单地发送一条消息到RabbitMQ消息队列。
  • 应用解耦合:系统中的各个模块(如订单模块、库存模块、支付模块等)之间需要进行数据交互和协同处理,但是由于各个模块之间的业务逻辑和数据格式不同,如果采用同步调用的方式,会导致系统之间高度耦合,一旦其中一个模块出现问题,整个系统的可用性都会受到影响。因此,为了实现系统之间的解耦,商城系统采用了异步消息处理的方式。
  • 分布式事务:订单数据持久化,更新API次数,持久化交互信息,日志收集等。

【ELK+KafkaELK由三个开源工具Elasticsearch、Logstash和Kibana组成。而Kafka是一个分布式事件流处理平台,能够高效地收集和处理大规模的数据流。具体实现步骤为:将所有设备上的日志统一收集到Kafka中,并使用Logstash从Kafka中消费数据并对其进行过滤和转换,最后将处理后的数据存储在Elasticsearch中。用户可以通过Kibana对Elasticsearch中的数据进行可视化展示和查询分析。这样,就能够方便地收集、管理、查看所有设备上的日志信息了。

【问题】MySQL中的表是怎么设计的?

【解决】秒杀用户表、商品信息表、秒杀商品表(记录该商品的秒杀始末时间,秒杀价和剩余量)、秒杀订单表(记录了秒杀用户名和秒杀的商品还有订单号)、订单详情表(通过秒杀订单号来查找对应的订单详情,里面记载更详实的业务信息)。

安全方面】

  • 使用双重MD5密码校验(对称加密),隐藏了秒杀接口地址,设置了接口限流防刷。
  • 最后还使用数学公式验证码不仅可以防恶意刷访问,还起到了削峰的作用。通过Jmeter压力测试,系统的QPS从150/s提升到2000/s。

【系统中的算法】

  • 排序算法:在商城系统中,排序算法通常用于对商品、订单等数据进行排序。常见的排序算法包括冒泡排序、插入排序、选择排序、快速排序、归并排序等。

  • 查找算法:在商城系统中,查找算法通常用于对商品、订单等数据进行查找。常见的查找算法包括线性查找、二分查找、哈希查找等。

  • 贪心算法:贪心算法可以用于商城系统中的促销活动等问题,例如如何在给定的促销金额下尽可能地吸引用户购买。

  • 动态规划算法:动态规划算法可以用于商城系统中的优惠券等问题,例如如何选择最优的优惠券组合以尽可能地减少用户支付金额。

  • 图算法:图算法可以用于商城系统中的推荐系统等问题,例如如何基于用户行为和商品属性构建推荐图谱,并根据推荐算法对用户进行商品推荐。

【Redis中的数据结构】

  • Hash:可以用来存储商品信息,如商品名称、价格、库存等。

  • List:可以用来实现队列,如订单队列,购物车列表等。

  • Set:可以用来实现商品分类、用户标签等功能。

  • zSet:可以用来实现商品排行榜、热门商品列表、秒杀商品等功能。

【系统中的数据结构】

  • 数组(Array):数组可以用来存储商品、订单等数据,使用索引进行访问。

  • 链表(List):链表可以用来实现购物车等需要动态添加和删除的数据结构。

  • 栈(Stack)和队列(Queue):栈和队列可以用来实现订单等需要按照时间顺序处理的数据结构。

  • 哈希表(Hash):哈希表可以用来存储用户信息、商品信息等数据,使用键值对进行访问。

  • 树(Tree):树可以用来实现商品分类等需要层级关系的数据结构。

  • 图(Graph):图可以用来实现商品推荐等需要复杂关系的数据结构。

【软件测试】

  • 压力测试:使用一些开源的压力测试工具,例如Apache JMeter或Gatling等。在测试过程中,可以逐渐增加并发请求的数量,直到系统达到瓶颈并出现性能问题为止。

  • 系统监控:通过监控CPU、内存、网络带宽、响应时间、吞吐量等,可以了解系统的负载情况和瓶颈所在,并及时采取相应的优化措施。

  • 负载均衡测试:将系统部署在多台服务器上,并使用负载均衡器进行均衡负载。通过模拟大量用户访问系统,并逐渐增加服务器数量,测试系统的并发处理能力和扩展性。

  • 安全测试:可以使用一些开源的安全测试工具,例如OWASP ZAP和Nmap等。

  • 内存泄漏测试:可以使用一些内存泄漏测试工具,例如Valgrind和JProfiler等。

1.8,系统架构

本系统采用严格的Java EE应用结构,其主要有如下几层:

  • 表现层:由Thymeleaf页面组成。
  • MVC层:使用Spring MVC框架。
  • 消息组件层:基于RabbitMQ实现。
  • 页面缓存层:由Redis负责管理。
  • 业务逻辑层:主要由Spring IoC容器管理的业务逻辑组件组成。
  • 数据缓存层:由Redis负责管理。
  • DAO层:由MyBatis Mapper组件组成。
  • 领域对象层:领域对象负责与结果集完成映射。
  • 数据库服务层:使用MySQL数据库存储持久化数据。

系统核心功能模块:用户模块和秒杀模块。系统包括4个Mapper对象:

  • UserMapper:提供对user_inf表的基本操作。
  • MiaoshaItemMapper:提供对item_inf表和miaosha_item表的基本操作。
  • OrderMapper:提供对order_inf表的基本操作。
  • MiaoshaOrderMapper:提供对order_inf表和miaosha_order的基本操作。

系统的两个业务逻辑组件:

  • UserService:提供用户登录、用户信息查看等业务逻辑功能的实现。
  • MiaoshaService:提供商品查看、商品秒杀等逻辑功能的实现。

系统的两个消息组件:

  • MiaoshaSender:该组件用于向RabbitMQ消息队列发送消息。
  • MiaoshaReceiver:该组件用于接收RabbitMQ消息队列中的消息。

系统的操作Redis组件:

  • FkRedisUtil,该组件基于Spring Data Redis实现。

系统组件关系:

2,项目搭建

本系统会用到如下框架和技术:

  • Spring MVC:由Spring Boot Web负责提供。
  • Thymeleaf:由Spring Boot Thymeleaf负责提供。
  • MyBatis:由MyBatis Spring Boot负责提供。由于本例需要访问MySQL数据库,因此还必须添加MySQL驱动库。
  • Redis:由Spring Boot Data Redis负责提供。在连接Redis时还需要依赖Apache Commons Pool2连接池。
  • RabbitMQ:由Spring Boot AMQP负责提供。
  • 此外,本系统还用到了Common Lang3和Commons Codec两个工具库,其中Common Lang 3提供了StringUtils,ArrayUtils,ClassUtils,RegExUtils等大量工具类,使用这些工具类所提供的静态方法会比较方便;Commons Codec 则包含一些通用的编码/解码算法,比如本系统所要使用的MD5加密算法。

Maven依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ysy</groupId>
    <artifactId>miaosha</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>miaosha</name>
    <description>miaosha</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Boot Thymeleaf -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- Spring Boot AMQP -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!-- Spring Boot Data Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 添加Apache Commons Pool2的依赖 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- MyBatis Spring Boot -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
        <!-- MySQL驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.23</version>
        </dependency>
        <!-- commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <!-- common-codec -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

applicationContext.properties

# -----------数据库有关的配置-----------
# 数据库驱动
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 数据库URL
spring.datasource.url=jdbc:mysql://localhost:3306/miaosha_app?serverTimezone=UTC
# 连接数据库的用户名
spring.datasource.username=root
# 连接数据库的密码
spring.datasource.password=123456
# -----------Redis有关的配置-----------
spring.redis.host=localhost
spring.redis.port=6379
# 指定连接Redis的DB0数据库
spring.redis.database=0
# 连接密码
spring.redis.password=32147
# 指定连接池中最大的活动连接数为20
spring.redis.lettuce.pool.maxActive = 20
# 指定连接池中最大的空闲连接数为20
spring.redis.lettuce.pool.maxIdle=20
# 指定连接池中最小的空闲连接数为2
spring.redis.lettuce.pool.minIdle = 2

3,领域对象层

3.1,设计领域对象

面向对象分析,是指根据系统需求提取应用中的对象,将这些对象抽象成类,再抽取出需要持久化保存的类,这些需要持久化保存的类就是领域对象。该系统并没有预先设计数据库,而是完全从面向对象分析开始设计了如下领域对象类:

  • User:对应于秒杀系统的用户。
  • Item:对应于秒杀系统中的商品,包含商品名称`商品描述等基本信息。
  • MiaoshaItem:对应于参与秒杀的商品,除包含基本的商品信息之外,还包含秒杀商品的秒杀价,库存,秒杀开始时间和秒杀结束时间等。
  • Order:对应于订单信息,用于保存订单用户,订单价格,下单时间等必要信息。
  • MiaoshaOrder:对应于秒杀订单,只保存了用户ID,订单ID,商品ID等基本信息。
drop database miaosha_app;
create database miaosha_app;
use miaosha_app;

秒杀用户表

drop table if exists user_inf;
create table user_inf
(
  user_id bigint primary key comment '手机号码作为用户ID',
  nickname varchar(255) not null,
  password varchar(32) comment '保存加盐加密后的密码:MD5(密码, salt)',
  salt varchar(10),
  head varchar(128) comment '头像地址',
  register_date datetime comment '注册时间',
  last_login_date datetime comment '上次登录时间',
  login_count int comment '登录次数'
) comment='秒杀用户表';

user_inf表使用用户手机号码作为唯一标识(主键),用户密码则以加盐加密(MD5加密)的形式保存,每个用户的“盐”可以不同。即使对于相同的密码,加不同的盐后得到的密码字符串也是不同的。所谓加盐(salt),就是在原有密码的基础上再拼接出一个其他字符串,这个其他字符串就是所谓的“盐”,然后使用加密算法对拼接后的字符串进行加密,这样得到的加密字符串就是所谓的加盐加密的密码。

insert into user_inf values
(13500008888, 'fkjava', '7fb8f847c09ad032fbf3e3b9fcd2101f', '0p9o8i', null, curdate(), curdate(), 1),
(13500006666, 'fkit', '7fb8f847c09ad032fbf3e3b9fcd2101f', '0p9o8i', null, curdate(), curdate(), 1),
(13500009999, 'crazyit', '7fb8f847c09ad032fbf3e3b9fcd2101f', '0p9o8i', null, curdate(), curdate(), 1);

商品表

drop table if exists item_inf;
create table item_inf
(
  item_id bigint primary key auto_increment comment '商品ID',
  item_name varchar(255) comment '商品名称',
  title varchar(64) comment '商品标题',
  item_img varchar(64) comment '商品的图片',
  item_detail longtext comment '商品的详情介绍',
  item_price decimal(10,2) comment '商品单价',
  stock_num int comment '商品库存,-1表示没有限制'
) comment='商品表';
insert into item_inf values
(1, '疯狂Java讲义', '行销几十万册,成为海峡两岸读者之选,赠送20+小时视频、源代码、课件、面试题,微信交流答疑群', 'books/java.png', '1)作者提供用于学习和交流的配套网站及作者亲自在线的微信群、QQ群。<br>2)《疯狂Java讲义》历时十年沉淀,现已升级到第5版,经过无数Java学习者的反复验证,被包括北京大学在内的大量985、211高校的优秀教师引荐为参考资料、选作教材。<br>3)《疯狂Java讲义》曾翻译为中文繁体字版,在宝岛台湾上市发行。<br>4)《疯狂Java讲义》屡获殊荣,多次获取电子工业出版社的“畅销图书”、“长销图书”奖项,作者本人也多次获得“优秀作者”称号。仅第3版一版的印量即达9万多册。', 139.00, 2000);
insert into item_inf values
(2, '轻量级Java Web企业应用实战——Spring MVC+Spring+MyBatis整合开发', '源码级剖析Spring框架,适合已掌握Java基础或学完疯狂Java讲义的读者,送配套代码、100分钟课程。进微信群', 'books/javaweb.png', '《轻量级Java Web企业应用实战――Spring MVC+Spring+MyBatis整合开发》不是一份“X天精通Java EE开发”的“心灵鸡汤”,这是一本令人生畏的“砖头”书。<h4>1. 内容实际,针对性强</h3>本书介绍的Java EE应用示例,采用了目前企业流行的开发架构,严格遵守Java EE开发规范,而不是将各种技术杂乱地糅合在一起号称Java EE。读者参考本书的架构,完全可以身临其境地感受企业实际开发。<h4>2.框架源代码级的讲解,深入透彻</h3>
本书针对Spring MVC、Spring、MyBatis框架核心部分的源代码进行了讲解,不仅能帮助读者真正掌握框架的本质,而且能让读者参考优秀框架的源代码快速提高自己的技术功底。本书介绍的源代码解读方法还可消除开发者对阅读框架源代码的恐惧,让开发者在遇到技术问题时能冷静分析问题,从框架源代码层次找到问题根源。<h4>3.丰富、翔实的代码,面向实战</h3>本书是面向实战的技术图书,坚信所有知识点必须转换成代码才能最终变成有效的生产力,因此本书为所有知识点提供了对应的可执行的示例代码。代码不仅有细致的注释,还结合理论对示例进行了详细的解释,真正让读者做到学以致用。', 139.00, 2300);
insert into item_inf values
(3, '疯狂Android讲义', 'Java语言实现,安卓经典之作,stormzhang刘望舒柯俊林启舰联合力荐,曾获评CSDN年度具有技术影响力十大原创图书', 'books/android.png', '<ul><li>《疯狂Android讲义》自面市以来重印30+次,发行量近20万册,并屡获殊荣!</li><li>开卷数据显示《疯狂Android讲义》曾位列Android图书年度排行榜三甲</li><li>《疯狂Android讲义》曾获评CSDN年度具有技术影响力十大原创图书</li><li>青年意见领袖StormZhang及多部Android牛书作者刘望舒、柯俊林、启舰联合力荐</li><li>多次荣获电子工业出版社年度畅销图书及长销图书大奖</li><li>被工信出版集团授予年度“优秀出版物”奖</li></ul>', 138.00, 2300);
insert into item_inf values
(4, '疯狂HTML 5/CSS3/JavaScript讲义', 'HTML 5与JavaScript编程的经典制作,前端开发的必备基础', 'books/html.png', '知名IT作家李刚老师力作,全书面向HTML5.1规范正式版,更新多个元素、拖放规范的相关知识,新增外挂字幕、点线模式等内容,着重介绍新增的手机端相关特性<br>详细介绍渐变背景支持、弹性盒布局、手机浏览器响应式布局、3D变换等CSS新增特性及重大改进', 99.00, 2300);
insert into item_inf values
(5, '疯狂Python讲义', '零基础学Python编程实战,CSDN爆款Python课程指定用书,覆盖爬虫、大数据、并发编程等就业热点,Python求职不再慌', 'books/python.png', '<ul><li>CSDN爆款课程“21天通关Python”指定用书。</li><li>京东科技IT新书榜探花之作,入选2019年度京东科技IT榜畅销榜</li><li>上手门槛低,8岁的小朋友Charlie亲验,不但可以看懂书中关于Python语法的基础知识,且写出了自己的小程序。</li><li>覆盖的知识面广,知识体系完备、系统,再也不用“面向百度”编程。</li></ul>', 118.00, 2300);
insert into item_inf values
(6, '轻量级Java EE企业应用实战——Struts 2+Spring+Hibernate5/JPA2整合开发', 'S2SH经典图书升级版,全面拥抱Spring 5轻量级Web开发新特性;面世十余年,历经数十万读者检验;', 'books/javaee.png', '<h4>1. 图书的附加值超燃</h3>DVD光盘中包含1000分钟超长视频、丰富代码等内容。<br>为读者提供用于学习交流的配套网站、微信群、QQ群。附赠107道各大企业Java EE面试题,覆盖Java Web、Struts 2、Hibernate、Spring、Spring MVC,助力叩开名企Java开发大门。<h4>2. 屡获殊荣</h3>本书曾荣获中国书刊发行业协会授予的“年度全行业YouXiu畅销品种”奖项,并多次荣获电子工业出版社授予的畅销书奖项,累计印刷40+次。', 139.00, 2300);

秒杀商品表

drop table if exists miaosha_item;
create table miaosha_item
(
  miaosha_id bigint primary key auto_increment comment '秒杀的商品表',
  item_id bigint comment '商品ID',
  miaosha_price decimal(10,2) comment '秒杀价',
  stock_count int comment '库存数量',
  start_date datetime comment '秒杀开始时间',
  end_date datetime comment '秒杀结束时间',
  foreign key(item_id) references item_inf(item_id)
) comment='秒杀商品表';

miaosha_item表的item_id外键列引用了item_inf表的item_id列,因此miaosha_item表只保存了秒杀商品的秒杀价,秒杀库存,秒杀开始时间和秒杀结束时间等信息。

insert into miaosha_item values (1, 1, 1.98, 8, adddate(curdate(), -1), adddate(curdate(), 3));
insert into miaosha_item values (2, 2, 2.98, 8, adddate(curdate(), -1), adddate(curdate(), 2));
insert into miaosha_item values (3, 3, 3.98, 8, adddate(curdate(), -3), adddate(curdate(), -1));
insert into miaosha_item values (4, 4, 4.98, 8, adddate(curdate(), 1), adddate(curdate(), 5));
insert into miaosha_item values (5, 5, 5.98, 8, adddate(curdate(), -1), adddate(curdate(), 2));
insert into miaosha_item values (6, 6, 6.98, 8, adddate(curdate(), -1), adddate(curdate(), 2));

订单表

drop table if exists order_inf;
create table order_inf
(
  order_id bigint primary key auto_increment,
  user_id bigint comment '用户ID',
  item_id bigint comment '商品ID',
  item_name varchar(255) comment '冗余的商品名称,用于避免多表连接',
  order_num int comment '购买的商品数量',
  order_price decimal(10,2) comment '购买价格',
  order_channel tinyint comment '渠道:1、PC, 2、Android, 3、iOS',
  order_status tinyint comment '订单状态,0新建未支付, 1已支付,2已发货, 3已收货, 4已退款,5已完成',
  create_date datetime comment '订单的创建时间',
  pay_date datetime comment '支付时间',
  foreign key(user_id) references user_inf(user_id),
  foreign key(item_id) references item_inf(item_id)
)  comment='订单表';

order_inf表用于保存基本的订单信息,包括用户ID,商品ID,购买数量,购买价格,下单时间,支付时间等信息。

秒杀订单表

普通时候,用户是可以对同一个商品重复下单的,但在系统进行秒杀活动时,不管是出于商业中饥饿营销的目的,还是出于公平性的考虑,通常都不允许用户对同一个商品进行重复秒杀,否则可能会导致在系统刚上线时同一个用户就将所有商品全部秒杀掉。因此,本系统要定义一个miaosha_order表,用于限制用户不能对同一个商品进行重复秒杀。

drop table if exists miaosha_order;
create table miaosha_order
(
  miaosha_order_id bigint primary key auto_increment,
  user_id bigint comment '用户ID',
  order_id bigint comment '订单ID',
  item_id bigint comment '商品ID',
  unique key(user_id, item_id),
  foreign key(user_id) references user_inf(user_id),
  foreign key(order_id) references order_inf(order_id),
  foreign key(item_id) references item_inf(item_id)
) comment='秒杀订单表';

从上面的建表语句可以看出,该SQL语句针对miaosha_order表的user_id和item_id两列的组合定义了唯一约束,这就限制了用户不能对同一个商品进行重复秒杀。

3.2,创建领域对象类

本系统使用MyBatis操作数据库,不过MyBatis并不是真正的ORM框架,它只是一个结果集映射框架,因此本系统所需要的领域对象只是一些简单的数据类。

public class User {
    private Long id;
    private String nickname;
    private String password;
    private String salt;
    private String head;
    private Date registerDate;
    private Date lastLoginDate;
    private Integer loginCount;
    //省略setter和getter方法
    ...
}
public class Item {
    private Long id;
    private String itemName;
    private String title;
    private String itemImg;
    private String itemDetail;
    private Double itemPrice;
    private Integer stockNum;
    //省略setter和getter方法
    ...
}
public class MiaoshaItem extends Item {
    private Long id;
    private Long itemId;
    private double miaoshaPrice;
    private Integer stockCount;
    private Date startDate;
    private Date endDate;
    //省略setter和getter方法
    ...
}
public class Order {
    private Long id;
    private Long userId;
    private Long itemId;
    private String itemName;
    private Integer orderNum;
    private Double orderPrice;
    private Integer orderChannel;
    private Integer status;
    private Date createDate;
    private Date payDate;
    //省略setter和getter方法
    ...
}
public class MiaoshaOrder {
    private Long id;
    private Long userId;
    private Long orderId;
    private Long itemId;
    //省略setter和getter方法
    ...
}

4,实现Mapper(DAO)层

4.1,概述

MyBatis的主要优势之一就是可以使用Mapper组件来充当DAO组件,开发者只需要简单地定义Mapper接口,并通过XML文件为Mapper接口中的方法提供对应的 SQL 语句,这样Mapper组件就开发完成了。

使用Mapper组件充当DAO组件,使用Mapper组件再次封装数据库操作,这也是Java EE应用中常用的DAO模式。当使用DAO模式时,既体现了业务逻辑组件封装Mapper组件的门面模式,也可分离业务逻辑组件和Mapper组件的功能:业务逻辑组件负责业务逻辑的变化,而Mapper组件负责持久化技术的变化。这正是桥接模式的应用。

当引入DAO模式后,每个Mapper组件都包含了数据库的访问逻辑;每个Mapper组件都可对一个数据库表完成基本的CRUD等操作。DAO模式是一种更符合软件工程的开发方式:

  • DAO模式抽象出数据访问方式,业务逻辑组件无须理会底层的数据库访问细节,其只专注于业务逻辑的实现。业务逻辑组件只负责业务功能的改变。
  • DAO将数据访问集中在独立的一层,所有的数据访问都由DAO组件完成,这层独立的DAO分离了数据访问的实现与其他业务逻辑,使得系统更具可维护性。
  • DAO还有助于提高系统的可移植性。独立的DAO层使得系统能在不同的数据库之间轻易切换,底层的数据库实现对于业务逻辑组件是透明的。数据库移植时仅仅影响DAO层,不同数据库之间的切换不会影响业务逻辑组件,因此提高了系统的可复用性。

4.2,实现Mapper组件

Mapper组件提供了对各持久化对象的基本的CRUD操作,而Mapper接口则负责声明该组件所应该包含的各种CRUD方法。

由于MyBatis Mapper组件中的方法并不会由框架自动提供,而是必须由开发者自行定义,并为之提供对应的SQL语句,因此Mapper组件中的方法可能会随着业务逻辑的需求而增加。

@Mapper
public interface UserMapper {
    // 根据user_id查询user_inf表的记录
    @Select("select user_id as id, nickname, password, salt, head, " +
            "register_date as registerDate, last_login_date as lastLoginDate, " +
            "login_count as loginCount from user_inf where user_id = #{id}")
    User findById(long id);

    // 更新user_inf表的记录
    @Update("update user_inf set last_login_date = #{lastLoginDate}" +
            ", login_count=#{loginCount} where user_id = #{id}")
    void update(User user);
}

@Mapper
public interface MiaoshaItemMapper {
    // 查询所有秒杀商品
    @Select("select it.*,mi.stock_count, mi.start_date, mi.end_date, " +
            "mi.miaosha_price from miaosha_item mi left join item_inf " +
            "it on mi.item_id = it.item_id")
    @Results(id = "itemMapper", value = {
            @Result(property = "itemId", column = "item_id"),
            @Result(property = "itemName", column = "item_name"),
            @Result(property = "title", column = "title"),
            @Result(property = "itemImg", column = "item_img"),
            @Result(property = "itemDetail", column = "item_detail"),
            @Result(property = "itemPrice", column = "item_price"),
            @Result(property = "stockNum", column = "stock_num"),
            @Result(property = "miaoshaPrice", column = "miaosha_price"),
            @Result(property = "stockCount", column = "stock_count"),
            @Result(property = "startDate", column = "start_date"),
            @Result(property = "endDate", column = "end_date")
    })
    List<MiaoshaItem> findAll();

    // 根据商品ID查询秒杀商品
    @Select("select it.*,mi.stock_count, mi.start_date, mi.end_date, " +
            "mi.miaosha_price from miaosha_item mi left join item_inf it " +
            "on mi.item_id = it.item_id where it.item_id = #{itemId}")
    @ResultMap("itemMapper")
    MiaoshaItem findById(@Param("itemId") long itemId);

    // 更新miaosha_item表中的记录
    @Update("update miaosha_item set stock_count = stock_count - 1" +
            " where item_id = #{itemId}")
    int reduceStock(MiaoshaItem miaoshaItem);
}
@Mapper
public interface OrderMapper {
    // 向order_inf表插入新的记录
    @Insert("insert into order_inf(user_id, item_id, item_name, order_num, " +
            "order_price, order_channel, order_status, create_date) values" +
            "(#{userId}, #{itemId}, #{itemName}, #{orderNum}, #{orderPrice}, " +
            "#{orderChannel}, #{status}, #{createDate})")
    // 指定获取向order_inf插入记录时所获取的自增长主键值
    @Options(useGeneratedKeys = true, keyProperty = "id")
    long save(Order order);

    // 根据订单ID和下单用户的ID来获取订单
    @Select("select order_id as id, user_id as userId, item_id as itemId, " +
            "item_name as itemName, order_num as orderNum, order_price as " +
            "orderPrice, order_channel as orderChannel, order_status as " +
            "status, create_date as createDate, pay_date as payDate from " +
            "order_inf where order_id = #{param1} and user_id = #{param2}")
    Order findByIdAndOwnerId(long orderId, long userId);
}
@Mapper
public interface MiaoshaOrderMapper {
    // 根据用户ID和商品ID获取秒杀订单
    @Select("select miaosha_order_id as id, user_id as userId, order_id as " +
            "orderId, item_id as itemId from miaosha_order " +
            "where user_id=#{userId} and item_id=#{itemId}")
    MiaoshaOrder findByUserIdItemId(@Param("userId") long userId,
                                    @Param("itemId") long itemId);

    // 插入秒杀订单
    @Insert("insert into miaosha_order(user_id, item_id, order_id) values " +
            "(#{userId}, #{itemId}, #{orderId})")
    int save(MiaoshaOrder miaoshaOrder);
}

4.3,部署Mapper组件

由于本系统是基于Spring Boot开发的,本系统已经添加了MyBatis Spring Boot依赖库,这意味着Spring Boot会为整合MyBatis提供自动配置,因此开发者只需要在application.properties文件中指定连接数据库的必要信息,Spring Boot就会自动在容器中配置数据源`SqlSessionFactory 等基础组件。有了这些基础组件之后,Spring Boot会自动扫描到Mapper接口上的@Mapper注解(上面所有Mapper接口上都添加了该注解),并将它们部署成容器中的Bean。

# -----------数据库有关的配置-----------
# 数据库驱动
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据库URL
spring.datasource.url=jdbc:mysql://localhost:3306/miaosha_app?serverTimezone=UTC
# 连接数据库的用户名
spring.datasource.username=root
# 连接数据库的密码
spring.datasource.password=32147
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐