gulimall` 项目致力于打造一个完整的电商系统,采用现阶段流行技术来实现,采用前后端分离继续编写。
gulimall(谷粒商城) 项目是一套电商项目,包括前台商城系统以及后台管理系统,基于 SpringCloud + SpringCloudAlibaba + MyBatis-Plus实现,采用 Docker 容器化部署。前台商城系统包括:用户登录、注册、商品搜索、商品详情、购物车、下订单流程、秒杀活动等模块。后台管理系统包括:系统管理、商品系统、优惠营销、库存系统、订单系统、用户系统、内容管理等七大模块。 分布式项目谷粒商城,前后端分离,前端基于Vue+ElementUI,后端基于SpringBoot+MybatisPlus+Mysql+Redis+ElasticSearch,具备商城所有功能(注册(社交登录)、登录、上架、检索、购物车、订单、支付、秒杀),项目框架由renrenfast开源项目搭建。项目中使用到SpringCache,SpringSession,Rabbitmq,SpringGateway+Nginx,Openfeign,alibaba-nacos做分布式注册中心和配置中心,alibaba-seata做分布式事务控制,redisson做布式锁。
gulimall
├── gulimall-common -- 工具类及通用代码
├── renren-generator -- 人人开源项目的代码生成器
├── gulimall-auth-server -- 认证中心(社交登录、OAuth2.0、单点登录)
├── gulimall-cart -- 购物车服务
├── gulimall-coupon -- 优惠卷服务
├── gulimall-gateway -- 统一配置网关
├── gulimall-order -- 订单服务
├── gulimall-product -- 商品服务
├── gulimall-search -- 检索服务
├── gulimall-seckill -- 秒杀服务
├── gulimall-third-party -- 第三方服务
├── gulimall-ware -- 仓储服务
└── gulimall-member -- 会员服务
后端技术
技术 | 说明 | 官网 |
---|---|---|
SpringBoot | 容器+MVC框架 | https://spring.io/projects/spring-boot |
SpringCloud | 微服务架构 | https://spring.io/projects/spring-cloud |
SpringCloudAlibaba | 一系列组件 | https://spring.io/projects/spring-cloud-alibaba |
MyBatis-Plus | ORM框架 | https://mp.baomidou.com |
renren-generator | 人人开源项目的代码生成器 | https://gitee.com/renrenio/renren-generator |
Elasticsearch | 搜索引擎 | https://github.com/elastic/elasticsearch |
RabbitMQ | 消息队列 | https://www.rabbitmq.com |
Springsession | 分布式缓存 | https://projects.spring.io/spring-session |
Redisson | 分布式锁 | https://github.com/redisson/redisson |
Docker | 应用容器引擎 | https://www.docker.com |
OSS | 对象云存储 | https://github.com/aliyun/aliyun-oss-java-sdk |
前端技术
技术 | 说明 | 官网 |
---|---|---|
Vue | 前端框架 | https://vuejs.org |
Element | 前端UI框架 | https://element.eleme.io |
thymeleaf | 模板引擎 | https://www.thymeleaf.org |
node.js | 服务端的js | https://nodejs.org/en |
系统架构图
业务架构图
工具 | 说明 | 官网 |
---|---|---|
IDEA | 开发Java程序 | https://www.jetbrains.com/idea/download |
RedisDesktop | redis客户端连接工具 | https://redisdesktop.com/download |
SwitchHosts | 本地host管理 | https://oldj.github.io/SwitchHosts |
X-shell | Linux远程连接工具 | http://www.netsarang.com/download/software.html |
Navicat | 数据库连接工具 | http://www.formysql.com/xiazai.html |
PowerDesigner | 数据库设计工具 | http://powerdesigner.de |
Postman | API接口调试工具 | https://www.postman.com |
Jmeter | 性能压测工具 | https://jmeter.apache.org |
Typora | Markdown编辑器 | https://typora.io |
工具 | 版本号 | 下载 |
---|---|---|
JDK | 1.8 | https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html |
Mysql | 5.7 | https://www.mysql.com |
Redis | Redis | https://redis.io/download |
Elasticsearch | 7.6.2 | https://www.elastic.co/downloads |
Kibana | 7.6.2 | https://www.elastic.co/cn/kibana |
RabbitMQ | 3.8.5 | http://www.rabbitmq.com/download.html |
Nginx | 1.1.6 | http://nginx.org/en/download.html |
注意:以上的除了jdk都是采用docker方式进行安装,详细安装步骤可参考百度!!!
Windows环境部署
192.168.77.130 gulimall.com
192.168.77.130 search.gulimall.com
192.168.77.130 item.gulimall.com
192.168.77.130 auth.gulimall.com
192.168.77.130 cart.gulimall.com
192.168.77.130 order.gulimall.com
192.168.77.130 member.gulimall.com
192.168.77.130 seckill.gulimall.com
以上端口换成自己Linux的ip地址
1、在nginx.conf中添加负载均衡的配置
upstream gulimall {
server 192.168.43.182:88;
}
2、在gulimall.conf中添加如下配置
server {
listen 80;
server_name gulimall.com *.gulimall.com hjl.mynatapp.cc;
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
#配置静态资源的动态分离
location /static/ {
root /usr/share/nginx/html;
}
#支付异步回调的一个配置
location /payed/ {
proxy_set_header Host order.gulimall.com; #不让请求头丢失
proxy_pass http://gulimall;
}
location / {
#root /usr/share/nginx/html;
#index index.html index.htm;
proxy_set_header Host $host; #不让请求头丢失
proxy_pass http://gulimall;
}
renren-fast-vue
以 npm run dev
方式去运行gulimall
,并导入 IDEA 中完成编译docker run --name rabbitmq -p 5672:5672 -p 15672:15672 -d rabbitmq:3.8-management
# 编写my.cnf配置文件
vim /root/docker/mysql/conf/my.cnf
[mysql]
# 设置mysql客户端默认字符集
default-character-set=utf8mb4
[client]
# 设置mysql客户端连接服务端时默认使用的端口
port=3306
default-character-set=utf8mb4
[mysqld]
# 设置3306端口
port=3306
# 允许最大连接数
max_connections=200
# 允许连接失败的次数。
max_connect_errors=10
# 服务端使用的字符集默认为utf8mb4
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
skip-name-resolve
# 运行容器
docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root \
-v /root/docker/mysql/conf:/etc/mysql/conf.d \
-v /root/docker/mysql/data:/var/lib/mysql \
-v /root/docker/mysql/log:/var/log/mysql \
-d mysql:8.0
docker pull redis:5.0.9
mkdir /root/docker/redis
vim /root/docker/redis/redis.conf
port 6379
requirepass xxxx
appendonly yes
docker run -p 6379:6379 --name redis \
-v /root/docker/redis/data:/data \
-v /root/docker/redis/redis.conf:/etc/redis/redis.conf \
-d redis:5.0.9 redis-server /etc/redis/redis.conf
mkdir /root/docker/nginx
mkdir /root/docker/nginx/conf
# 由于我们现在没有配置文件,也不知道配置什么。可以先启动一个nginx,讲他的配置文件拷贝出来
# 再作为映射,启动真正的nginx
docker pull nginx:1.17.4
docker run --name some-nginx -d nginx:1.17.4
docker container cp some-nginx:/etc/nginx /root/docker/nginx/conf
# 然后就可以删除这个容器了
docker docker rm -f some-nginx
# 启动nginx
docker run --name nginx -p 80:80 \
-v /root/docker/nginx/conf:/etc/nginx \
-v /root/docker/nginx/html:/usr/share/nginx/html \
-d nginx:1.17.4
docker pull elasticsearch:7.8.0
# 做映射之前赋予文件夹相应权限,
chmod -R 775 /root/docker/elasticsearch/data
chmod -R 775 /root/docker/elasticsearch/logs
docker run -p 9200:9200 -p 9300:9300 --name es7.8 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms128m -Xmx512m" \
-v /root/docker/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-v /root/docker/elasticsearch/data:/usr/share/elasticsearch/data \
-v /root/docker/elasticsearch/logs:/usr/share/elasticsearch/logs \
-d elasticsearch:7.8.0
# 拉取镜像
# kibana版本必须和elasticsearch版本保持一致
docker pull kibana:7.8.0
# 启动容器
# YOUR_ELASTICSEARCH_CONTAINER_NAME_OR_ID 正在运行的ES容器ID或name
docker run --link YOUR_ELASTICSEARCH_CONTAINER_NAME_OR_ID:elasticsearch -p 5601:5601 {docker-repo}:{version}
docker run --link es7.8:elasticsearch -p 5601:5601 --name kibana -d kibana:7.8.0
# Ik分词器版本要和ES和Kibana版本保持一致
# 进入容器
docker exec -it es7.8 /bin/bash
#此命令需要在容器中运行
elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.8.0/elasticsearch-analysis-ik-7.8.0.zip
# 退出容器,重启容器
exit
docker restart es7.8
<properties>
<springcloud.alibaba.version>2.2.2.RELEASE</springcloud.alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
# 服务端口
server.port=8070
# 服务名
spring.application.name=service-provider
# 注册中心地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
@SpringBootApplication
@EnableDiscoveryClient
public class NacosProviderApplication {
public static void main(String[] args) {
SpringApplication.run(NacosProviderApplication.class, args);
}
}
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
# 服务名,配置服务名是因为服务名是默认读取的dataId的一部分
spring.application.name=service-provider
# 配置中心地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
当配置了nacos配置中心,服务启动时会去nacos配置中心加载默认命名空间(public)下默认命名的配置集,dataId的组成形式是 ${prefix}-${spring.profiles.active}.${file-extension}
@RestController
@RequestMapping("/config")
@RefreshScope
public class ConfigController {
@Value("${useLocalCache:false}")
private boolean useLocalCache;
@RequestMapping("/get")
public boolean get() {
return useLocalCache;
}
}
https://redis.io/topics/distlock https://github.com/redisson/redisson
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。 其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
导入erdisson依赖,可去maven仓库
编写配置类,创建 RedissonClient对象
@autowired注入RedissonClient对象
获取锁 参数就是锁的名字 // 获取分布式可重入锁,最基本的锁 RLock lock = redissonClient.getLock("锁名"); // 获取读写锁 redissonClient.getReadWriteLock("anyRWLock"); // 信号量 redissonClient.getSemaphore("semaphore");
Rlock实现了juc下的lock,完全可以像使用本地锁一样使用它
以可重入锁为例 如果直接执行 lock.lock(); Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,(定时任务)不断的延长锁的有效期。 默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定 也就是说,先加锁,然后执行业务,锁的默认有效期是30s,业务进行期间,会通过定时任务不断将锁的有效期续至30s。直到业务代码结束 所以即便不手动释放锁。最终也会自动释放 默认是任务调度的周期是 看门狗时间 / 3 = 10s
也可以使用 lock.lock(10, TimeUnit.SECONDS);手动指定时间 此时,不会有定时任务自动延期,超过这个时间后锁便自动解开了 需要注意的是,如果代码块中有手动解锁,但是业务执行完成之前锁的有效期到了, 此时执行unlock会报错:当前线程无法解锁 因为现在redis中的锁是另一个线程加上的,而他的删锁逻辑是lua脚本执行 先获取键值,判断是否是自己加的锁。如果是。则释放,lua脚本保证这是一个原子操作 所以,手动设置时间必须保证这个时间内业务能够执行完成
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
static class CacheConfigurationImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
CacheType[] types = CacheType.values();
String[] imports = new String[types.length];
for (int i = 0; i < types.length; i++) {
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
return imports;
}
}
// 缓存类型
public enum CacheType {
GENERIC,
JCACHE,
EHCACHE,
HAZELCAST,
INFINISPAN,
COUCHBASE,
REDIS,
CAFFEINE,
SIMPLE,
NONE;}
// 各种类型缓存对应的自动配置类
mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);
mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
MAPPINGS = Collections.unmodifiableMap(mappings);
// 创建cacheManager。初始化cache
@Bean
RedisCacheManager cacheManager(){}
// 用于决定初始化cache用什么配置,如果用户自定义了RedisCacheConfiguration。就用用户的配置
// 否则就自己创建一个配置
private determineConfiguration(){
CacheProperties cacheProperties,
ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
ClassLoader classLoader) {
return redisCacheConfiguration.getIfAvailable(() -> createConfiguration(cacheProperties, classLoader));
}
// 自己创建一个RedisCacheConfiguration
private createConfiguration(
CacheProperties cacheProperties, ClassLoader classLoader) {
Redis redisProperties = cacheProperties.getRedis();
// 这里就是默认策略的设置
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
// 值的序列化采用Jdk序列化
config = config.serializeValuesWith(
SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
// 读取配置文件中用户指定的缓存有效期
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
// 读取配置文件中用户指定的缓存键的前缀
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
// 读取配置文件中用户指定的缓存是否要缓存空值,缓存控制能够解决缓存雪崩(疯狂访问一个缓存和数据库中都没有的id,导致崩溃)
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
// 读取配置文件中用户指定的缓存是否使用指定的键前缀
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
spring.cache.type=redis
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Cacheable: Triggers cache population: 触发将值存入缓存的操作
@CacheEvict: Triggers cache eviction. 触发将值从缓存移除的操作
@CachePut: Updates the cache without interfering with the method execution:触发更新缓存的操作
@Caching: Regroups multiple cache operations to be applied on a method:组合以上多种操作
@CacheConfig: Shares some common cache-related settings at class-level:在类级别上共享相同的缓存配置
/**
* @Cacheable 代表当前方法的结果需要放入缓存
* 并且,每次访问这个方法时,会先去缓存中判断数据是否存在,若存在则直接返回缓存中数据,不会执行方法
* 但是我们需要指定,要将结果放入哪个缓存中,每个cacheManager管理多个cache.
* 我们需要指定cache的名字(相当于对缓存进行分区管理,建议按照业务进行划分)
* 使用cacheNames或value属性都可以。可以同时放入多个分区
*
* 存入缓存中的键名:product-category::SimpleKey []
* cacheName::默认生成的键名 [方法参数列表]
* 这个simplekey[]就是自己生成的键名
* 我们可以使用注解的key属性,指定键名,它接收一个Spel表达式
* 比如 #root.methodName 就是获取方法名
* 详细用法:https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/integration.html#cache-spel-context
* 指定key后,得到的键名是:product-category::getLevel1Category
*
* 默认生成的值是采用jdk序列化,并且过期时间是-1,
* 如果我们想要改变默认配置,
* 一些最基本的配置可以在配置文件中设置:
* # 是否缓存空值
* spring.cache.redis.cache-null-values=true
* # 键的前缀
* spring.cache.redis.key-prefix=CACHE_
* # 是否使用这个前缀
* spring.cache.redis.use-key-prefix=true
* # 缓存的有效期
* spring.cache.redis.time-to-live=3600000
* 比较高级的配置,比如设置序列化策略采用json形式,就得自己编写RedisCacheConfiguration
sync 属性:默认为false。
如果设为true。则会为处理过程加上synchronized本地锁。也就是说在一个微服务里面,能够保证线程间互斥访问这个方法
*
* @return
*/
@RequestMapping("/product/category/level1")
@Cacheable(cacheNames = {"product-category"}, key = "#root.methodName")
public List<CategoryEntity> getLevel1Category() {
System.out.println("查询数据库");
List<CategoryEntity> level1Categories = categoryService.getLevel1Categories();
return level1Categories;
}
/**
* 自己编写RedisCacheConfiguration
*/
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
public class RedisCacheConfig {
/**
* 没有无参构造方法。不能直接 new RedisCacheConfiguration();
* 使用RedisCacheConfiguration.defaultCacheConfig();默认配置,再修改
*
* 用户的自定义配置都在 application.yml中其中spring.cache部分。
* 它和CacheProperties.class进行了绑定,
* @ConfigurationProperties(prefix = "spring.cache")
* public class CacheProperties {}
* 但是这个类它没有被作为一个bean放入容器中。
* 我们需要手动导入
* @EnableConfigurationProperties(CacheProperties.class)
* 这样容器中就会创建出一个 cacheProperties对象。我们可以使用方法参数直接接收
* @return
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 设置键的序列化策略
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()));
// 设置值的序列化策略
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()));
// 加载配置文件中用户自定义配置
// 拿出redis部分
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
/**
* @CacheEvict: 触发删除缓存操作
* cacheNames: 指定要删除的是哪个缓存分区的缓存
* key: 要删除的是这个分区中的那个缓存
* allEntries: 是否删除这个分区中的所有缓存。默认false
* 所以。如果只指定cacheNames.不指定key。不会进行任何删除。
* 如果设置allEntries = true。那么不用指定key。全部删除
*
* key。一次只能指定一个key。那如果要删除多个,但不想全部删除,就需要使用 @Caching
* @Caching(
* evict = {
* @CacheEvict(cacheNames = {ProductConstant.CacheName.PRODUCT_CATEGORY},
* key = "'level1Category'"),
* @CacheEvict(cacheNames = {ProductConstant.CacheName.PRODUCT_CATEGORY},
* key = "'level2Category'")
* },
* cacheable = {}
* )
*/
/**
* @CachePut
* 最简单的保证缓存和数据库一致性的方式就是,每次执行更新操作。就将缓存删除。下次查询方法自然会将最新数据存入缓存
*
* 如果我们的更新方法没有返回值,也就是更新完就结束,那么我们使用@CacheEvit删除缓存
* 如果我们的更新方法有返回值,也就是更新成功后将最新数据返回,那么我们使用@CachePut将最新数据更新到缓存
* @return
*/
https://codeonce.cc/archives/rabbitmq-confirm
https://codeonce.cc/archives/feign-lost-cookie
https://blog.csdn.net/alinyua/article/details/108106706
Below are the options to see the classes that are loaded in the Metaspace: 1. -verbose:class
2. -Xlog:class+load
3. jcmd GC.class_histogram
4. Programmatic approach
5. Heap Dump analysis
Let’s discuss each option in detail in this post.
-Xmx100m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc-product-%t.log
# 提交:主要 type
feat: 增加新功能
fix: 修复bug
# 提交:特殊 type
docs: 只改动了文档相关的内容
style: 不影响代码含义的改动,例如去掉空格、改变缩进、增删分号
build: 构造工具的或者外部依赖的改动,例如webpack,npm
refactor: 代码重构时使用
revert: 执行git revert打印的message
# 提交:暂不使用type
test: 添加测试或者修改现有测试
perf: 提高性能的改动
ci: 与CI(持续集成服务)有关的改动
chore: 不修改src或者test的其余修改,例如构建过程或辅助工具的变动
# 注释:类注释配置
/**
* @description:
* @author: ${USER}
* @date: ${DATE}
*/
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。