搜索下单(es、mysql、MQ同步;关于事务失效)
目录
搜索下单:
一、环境配置
1.MySQL配置:
2.RabbitMQ配置:
3.Canal配置:
测试MySQL-Canal-MQ
4.ES配置:
代码讲解:
管理同步表:
1)区域服务上架
2)区域服务下架
二、搜索服务:
1)需求分析
2)代码开发
三、预约下单
远程接口:
关于事务失效:
事务失效的情况有哪些?
搜索下单:
在当前项目中,有两处使用到了搜索服务:
-
门户最上端的搜索框(根据关键词搜索服务信息)
-
全部服务界面,点击分类名称搜索分类下的服务项目(根据服务分类id搜索服务项目)

如果要实现此搜索功能,ES肯定是第一选择,因为单纯的数据库查询从能力和性能上都无法满足需求
如果要从ES中进行数据的检索,就必须保证数据库中的参与搜索的这部分数据实时同步到ES中
此功能的实现大体有下面这些方案:
-
同步双写:在程序在中同时向MySQL和ES写数据,这种方式实现简单,但是代码耦合
-
异步消息:程序在向MySQL写入数据之后,向MQ中投递消息,ES相关程序监听MQ,获取数据写入ES
-
Canal监听:使用Canal监听MySQL的binlog,当发现写入操作后,立即读取到新写入的内容,并同步到ES
Canal的工作原理
Canal伪装自己为MySQL的从节点,向MySQL主节点发送dump协议
MySQL主节点一旦收到dump请求,开始推送binlog给canal
Canal会接收并解析这些变更事件并解析binlog,并发送到其它服务器(比如es、redis等等)

我们项目中采用的是将后两种方案相结合的方式,也就是使用Canal+MQ来保证MySQL跟ES中数据的一致性,具体思路如下:
-
运营人员对服务信息相关表(serve、serve_item、serve_type)进行增删改操作,MySQL记录binlog日志
-
Canal读取binlog解析出增加、修改、删除数据的记录,然后发送到MQ中
-
同步程序监听MQ,收到增加、修改、删除数据的记录,请求ES创建、修改、删除索引数据
-
小程序端用户请求服务搜索接口从ES中搜索服务信息

这里为了方便数据同步,我们单独设计了一张数据表serve_sync用来保存所有需要同步的数据
也就是说当我们对serve、serve_type、serve_item中相关字段进行操作的时候,也要同步修改一下serve_sync表
这样一来,我们就只需要将serve_sync中的数据同步到es中就可以了

总结一下我们要做的事情:
-
保证从MySQL-Canal-MQ-ES这套环境是没问题的
-
保证serve、serve_item、serve_type表数据变化的时候,serve_sync表中的数据要同步变化
-
保证小程序端用户可以从ES中搜索数据
一、环境配置
虽然我们已经在虚拟机当中配置好了,但还是需要知道每一项配置在做什么
1.MySQL配置:
① 在MySQL中需要创建一个用户,并授权
# 进入mysql容器
docker exec -it mysql /bin/bash# 使用命令登录
mysql -u root -p# 创建用户 用户名:canal 密码:canal
create user 'canal'@'%' identified WITH mysql_native_password by 'canal';# 授权
# SELECT:允许用户查询(读取)数据库中的数据
# REPLICATION SLAVE:允许用户作为 MySQL 复制从库,用于同步主库的数据
# REPLICATION CLIENT: 允许用户连接到主库并获取关于主库状态的信息
GRANT SELECT,REPLICATION SLAVE,REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
② 修改MySQL配置文件/usr/mysql/conf/my.cnf,开启Binlog功能
[mysqld]
# 打开binlog
log-bin=mysql-bin
# 选择ROW(行)模式: 以行为单位记录每个被修改的行的变更
binlog-format=ROW
# 配置MySQL replaction需要定义,不要和canal的slaveId重复
server_id=1# 只保留最近3天的日志
expire_logs_days=3
# binlog每个日志文件的大小
max_binlog_size = 100m
# binlog缓存区的大小
max_binlog_cache_size = 512m
配置文件修改完毕之后,使用下面命令重启mysql容器
docker restart mysql
③ 查看当前mysql的状态
-- 查看目前的binlog模式
SHOW VARIABLES LIKE 'log_bin'; -- ON
show variables like 'binlog_format'; -- ROW-- 查看binlog日志文件列表
SHOW BINARY LOGS;-- 查看当前正在写入的binlog文件
SHOW MASTER STATUS;
2.RabbitMQ配置:
目前虚拟机中提前准备好了内容有
-
虚拟主机:/xzb
-
虚拟主机对应的账户和密码:xzb
-
top类型的交换机:exchange.canal-jzo2o,以及对应队列的绑定

3.Canal配置:
① 配置MySQL的连接信息,修改/data/soft/canal/instance.properties
# 设置要监听的mysql服务器地址
canal.instance.master.address=192.168.101.68:3306# 设置binlog同步开始位置
canal.instance.master.journal.name=mysql-bin.000001
canal.instance.master.position=0
canal.instance.master.timestamp=
canal.instance.master.gtid=# 从MySQL同步时,用到的账号和密码
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
② 配置RabbitMQ的连接信息,修改/data/soft/canal/canal.properties
# tcp, kafka, rocketMQ, rabbitMQ
canal.serverMode = rabbitMQ##################################################
######### RabbitMQ #############
##################################################
# 下面配置分别是mq主机的地址、虚拟主机名称、交换机名称、账户、密码、持久化方式
rabbitmq.host = 192.168.101.68
rabbitmq.virtual.host = /xzb
rabbitmq.exchange = exchange.canal-jzo2o
rabbitmq.username = xzb
rabbitmq.password = xzb
rabbitmq.deliveryMode = 2
③ 配置需要监听的mysql表以及同步的mq的信息,修改/data/soft/canal/instance.properties
# 需要监听的数据库的表
# canal.instance.filter.regex=数据库名称\\数据表名称
# canal.instance.filter.regex=jzo2o-foundations\\.serve_sync
canal.instance.filter.regex=jzo2o-orders-1\\.orders_dispatch,jzo2o-orders-1\\.orders_seize,jzo2o-foundations\\.serve_sync,jzo2o-customer\\.serve_provider_sync,jzo2o-orders-1\\.serve_provider_sync,jzo2o-orders-1\\.history_orders_sync,jzo2o-orders-1\\.history_orders_serve_sync,jzo2o-market\\.activity# 需要将对应表的变化,通知到交换机哪个routingKey上
# canal.mq.dynamicTopic=交换机的routingKey:监听的数据库和表
# canal.mq.dynamicTopic=canal-mq-jzo2o-foundations:jzo2o-foundations\\.serve_sync
canal.mq.dynamicTopic=canal-mq-jzo2o-orders-dispatch:jzo2o-orders-1\\.orders_dispatch,canal-mq-jzo2o-orders-seize:jzo2o-orders-1\\.orders_seize,canal-mq-jzo2o-foundations:jzo2o-foundations\\.serve_sync,canal-mq-jzo2o-customer-provider:jzo2o-customer\\.serve_provider_sync,canal-mq-jzo2o-orders-provider:jzo2o-orders-1\\.serve_provider_sync,canal-mq-jzo2o-orders-serve-history:jzo2o-orders-1\\.history_orders_serve_sync,canal-mq-jzo2o-orders-history:jzo2o-orders-1\\.history_orders_sync,canal-mq-jzo2o-market-resource:jzo2o-market\\.activity
测试MySQL-Canal-MQ
接下来我们来测试一下:
在测试之前,我们需要将binlog日志重置一下,以方便canal从mysql-bin.000001文件开始读取内容
① 重置MySQL的binlog
-- 重置mysql主节点,它会删除所有的binlog,然后重新从000001号开始记录日志
reset master;-- 查看结果显示 mysql-bin.000001为正常
show master status;

② 删除meta.dat文件,重启canal
-- 停止docker
docker stop canal-- 删除meta.dat文件(此文件是用于存储canal读取mysql中binlog的偏移量)
rm -rf /data/soft/canal/conf/meta.dat-- 启动canal
docker start canal
退出mysql后输入以上指令:

③ 测试
修改jzo2o-foundations数据库的serve_sync表的数据,查看canal-mq-jzo2o-foundations队列,如果队列中有的消息说明同步成功



4.ES配置:
目前已经将数据同步到了MQ中,接下来我们编写一个程序从mq中监听数据,然后写入到es中
在这个es中已经创建好了对应的索引库serve_aggregation,结构如下
PUT /serve_aggregation
{"mappings" : {"properties" : {"city_code" : {"type" : "keyword"},"detail_img" : {"type" : "text","index" : false},"hot_time_stamp" : {"type" : "long"},"id" : {"type" : "keyword"},"is_hot" : {"type" : "short"},"price" : {"type" : "double"},"serve_item_icon" : {"type" : "text","index" : false},"serve_item_id" : {"type" : "keyword"},"serve_item_img" : {"type" : "text","index" : false},"serve_item_name" : {"type" : "text","analyzer": "ik_max_word","search_analyzer":"ik_smart"},"serve_item_sort_num" : {"type" : "short"},"serve_type_icon" : {"type" : "text","index" : false},"serve_type_id" : {"type" : "keyword"},"serve_type_img" : {"type" : "text","index" : false},"serve_type_name" : {"type" : "text","analyzer": "ik_max_word","search_analyzer":"ik_smart"},"serve_type_sort_num" : {"type" : "short"}}}
}
接下来我们启动虚拟机中的kibana软件:
docker start kibana7.17.7
然后使用下面地址访问kibana
http://192.168.101.68:5601
最后查看目前的索引库信息
# 查看索引库
GET /serve_aggregation# 查看索引库中的数据
GET /serve_aggregation/_search
{"query": {"match_all": {}}
}# 查看 1686352662791016449记录
GET /serve_aggregation/_doc/1686352662791016449
① 在foundations工程添加下边的依赖
<dependency><groupId>com.jzo2o</groupId><artifactId>jzo2o-canal-sync</artifactId>
</dependency>
<dependency><groupId>com.jzo2o</groupId><artifactId>jzo2o-es</artifactId>
</dependency>
② 修改foundations的配置文件,连接es和mq

③ 修改nacos中es的配置

④ 修改nacos中rabbitmq的配置

⑤ 实现数据同步
在jzo2o-foundations模块中添加com.jzo2o.foundations.handler.ServeCanalDataSyncHandler
负责监听mq,然后将消息同步到es中
package com.jzo2o.foundations.handler;import com.jzo2o.canal.listeners.AbstractCanalRabbitMqMsgListener;
import com.jzo2o.es.core.ElasticSearchTemplate;
import com.jzo2o.foundations.constants.IndexConstants;
import com.jzo2o.foundations.model.domain.ServeSync;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.List;//本类负责解析MQ中同步的serve_sync这张表的数据,然后写入到ES的serve_aggregation索引库
//后面相似的类有很多,例如order_sync、user_sync, 每个类中所做事情的步骤是一样的
//1. 解析MQ中的消息内容,封装到实体类对象(所有类中的这部分代码是一样的)
//2. 将实体类对象的数据同步到ES索引库中(每个类中这部分代码是不一样的)
@Component
public class ServeCanalDataSyncHandler extends AbstractCanalRabbitMqMsgListener<ServeSync> {@Resourceprivate ElasticSearchTemplate elasticSearchTemplate;//此方法会接收到到来自mq的消息,然后按照解析消息、同步索引库的步骤执行@RabbitListener(bindings = @QueueBinding(value = @Queue(name = "canal-mq-jzo2o-foundations",arguments = {@Argument(name = "x-single-active-consumer", value = "true", type = "java.lang.Boolean")}),exchange = @Exchange(name = "exchange.canal-jzo2o", type = ExchangeTypes.TOPIC),key = "canal-mq-jzo2o-foundations"))public void onMessage(Message message) throws Exception {//由于解析逻辑,所有子类的逻辑是一样的,索引自己放到父类中,此处直接调用父类的解析方法parseMsg(message);}//由于不同的子类实现逻辑不一样,因此每个子类自己实现此方法@Overridepublic void batchSave(List<ServeSync> data) {//保存数据到ES指定的索引库elasticSearchTemplate.opsForDoc().batchInsert(IndexConstants.SERVE, data);}//由于不同的子类实现逻辑不一样,因此每个子类自己实现此方法@Overridepublic void batchDelete(List<Long> ids) {//删除ES指定的索引库中的数据elasticSearchTemplate.opsForDoc().batchDelete(IndexConstants.SERVE, ids);}
}
效果展示:

代码讲解:
模版方法设计模式
适用场景:
一个功能的完成需要一系列的步骤,这些步骤是固定的,但是某些步骤的具体实现是待定的
比如:老师和学生在上午的经历步骤是一样的,都是吃早饭、工作、吃午饭
但是其中工作的内容是不一样的,老师的工作是讲课,学生的工作是学习
实现流程:
定义一个抽象类作为父类,在父类中提供模版方法
在模板方法中定义出流程的步骤,能确定的行为直接写出,不能确定的行为定义为抽象方法
根据不同的行为定义不同的子类,继承抽象类,重写父类的抽象方法,完成各自的行为内容

由于解析MQ中的消息内容并封装到实体类对象当中属于所有类共通的代码,因此我们写在父类当中并给予实现;而将实体类对象的数据同步到ES索引库中是每个类当中不同的,因此我们在父类当中定义该抽象方法,具体实现由其子类完成


可以看到我们配置完成了,es的数据同mysql变化
管理同步表:
目前已经完成了从MySQL到ES的同步流程,当serve_sync表变化时就会触发同步,什么时候serve_sync表变化呢
当服务信息变更时需要同时修改serve_sync表,下边是serve_sync表的变化时机
-
向serve_sync表添加记录:区域服务上架
-
从serve_sync表删除记录:区域服务下架
-
修改serve_sync表中记录:修改服务项、服务分类、服务价格、设置热门、取消热门(实战)
1)区域服务上架
修改区域服务上架的方法(在ServeServiceImpl.onSale),在方法的最后添加同步数据表的逻辑,为了代码清晰,可以单独抽取一个方法
@Transactional
@Override
public void onSale(Long id) {//3) 执行修改 。。。代码省略//4)添加同步表数据addServeSync(id);
}@Autowired
private ServeTypeMapper serveTypeMapper;@Autowired
private ServeSyncMapper serveSyncMapper;/*** 新增服务同步数据** @param serveId 服务id*/
private void addServeSync(Long serveId) {//服务信息Serve serve = baseMapper.selectById(serveId);//区域信息Region region = regionMapper.selectById(serve.getRegionId());//服务项信息ServeItem serveItem = serveItemMapper.selectById(serve.getServeItemId());//服务类型ServeType serveType = serveTypeMapper.selectById(serveItem.getServeTypeId());ServeSync serveSync = new ServeSync();serveSync.setServeTypeId(serveType.getId());serveSync.setServeTypeName(serveType.getName());serveSync.setServeTypeIcon(serveType.getServeTypeIcon());serveSync.setServeTypeImg(serveType.getImg());serveSync.setServeTypeSortNum(serveType.getSortNum());serveSync.setServeItemId(serveItem.getId());serveSync.setServeItemIcon(serveItem.getServeItemIcon());serveSync.setServeItemName(serveItem.getName());serveSync.setServeItemImg(serveItem.getImg());serveSync.setServeItemSortNum(serveItem.getSortNum());serveSync.setUnit(serveItem.getUnit());serveSync.setDetailImg(serveItem.getDetailImg());serveSync.setPrice(serve.getPrice());serveSync.setCityCode(region.getCityCode());serveSync.setId(serve.getId());serveSync.setIsHot(serve.getIsHot());serveSyncMapper.insert(serveSync);
}
2)区域服务下架
修改区域服务下架的方法(在ServeServiceImpl.offSale),在方法的最后添加同步数据表的逻辑

效果展示:


二、搜索服务:
1)需求分析
下面两处都需要从es中查询数据
-
搜索框:根据用户输入的内容搜索服务类型名称和服务项名称,注意要进行分词搜索
-
左侧栏:根据用户点击的服务类型id搜索
注意:无论是哪种搜索都要限定在指定的城市下进行,查到的服务项目按照serve_item_sort_num正序排列

这也就意味着当前功能包含两个查询分支
-
根据city_code和serve_type_id进行查询, 按照serve_item_sort_num正序排列
-
根据city_code和serve_item_name或者serve_type_name进行查询, 按照serve_item_sort_num正序排列
根据需求,可以写出DSL语句
# 根据city_code和serve_type_id进行查询, 按照serve_item_sort_num正序排列
GET /serve_aggregation/_search
{"query": {"bool": {"must": [{"term": {"city_code": {"value": "010"}}},{"term": {"serve_type_id": {"value": "1678649931106705409"}}}]}},"sort": [{"serve_item_sort_num": {"order": "asc"}}]
}# 根据city_code和serve_item_name或者serve_type_name进行查询, 按照serve_item_sort_num正序排列
GET /serve_aggregation/_search
{"query": {"bool": {"must": [{"term": {"city_code": {"value": "010"}}},{"multi_match": {"query": "保洁","fields": ["serve_item_name","serve_type_name"]}}]}},"sort": [{"serve_item_sort_num": {"order": "asc"}}]
}
2)代码开发
接下来我们根据DSL语句书写java代码:
接口路径:GET /foundations/customer/serve/search

ServeController:
@GetMapping("/search")
@ApiOperation("服务搜索")
public List<ServeSimpleResDTO> search(String cityCode, String keyword, Long serveTypeId) {return serveService.search(cityCode, keyword, serveTypeId);
}
IServeService:
/*** 服务搜索** @param cityCode 城市编码* @param keyword 关键词* @param serveTypeId 服务类型id* @return 服务项目信息*/
List<ServeSimpleResDTO> search(String cityCode, String keyword, Long serveTypeId);
ServeServiceImpl:
@Autowired
private RestHighLevelClient client;@Override
public List<ServeSimpleResDTO> search(String cityCode, String keyword, Long serveTypeId) {//1. 创建请求对象SearchRequest request = new SearchRequest("serve_aggregation");//2. 封装请求参数BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();//城市编码boolQuery.must(QueryBuilders.termQuery("city_code", cityCode));//服务类型idif (serveTypeId != null) {boolQuery.must(QueryBuilders.termQuery("serve_type_id", serveTypeId));}//关键词if (StrUtil.isNotEmpty(keyword)) {boolQuery.must(QueryBuilders.multiMatchQuery(keyword, "serve_item_name", "serve_type_name"));}request.source().query(boolQuery);//查询request.source().sort("serve_item_sort_num", SortOrder.ASC);//排序//3. 执行请求SearchResponse response = null;try {response = client.search(request, RequestOptions.DEFAULT);} catch (IOException e) {throw new RuntimeException(e);}//4. 处理返回结果 List<ServeSimpleResDTO>if (response.getHits().getTotalHits().value == 0) {return List.of();}return Arrays.stream(response.getHits().getHits()).map(e -> JSONUtil.toBean(e.getSourceAsString(), ServeSimpleResDTO.class)).collect(Collectors.toList());
}
新建EsConfiguration:
package com.jzo2o.foundations.config;import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class EsConfiguration {@Beanpublic RestHighLevelClient client(){return new RestHighLevelClient(RestClient.builder(new HttpHost("192.168.101.68",9200,"http")));}
}
效果展示:


刚才遇到一个报错:
Failed to convert value of type 'java.lang.String' to required type 'java.lang.Long'; nested exception is java.lang.NumberFormatException: For input string: "undefined"
说是我的前端请求参数为String类型而后端接收参数确是Long类型;
这是由于我将Controller代码错误地写到operation包下,路径就变成了:
http://192.168.101.1:11500/foundations/operation/serve/search;
而前端请求的是:
http://192.168.101.1:11500/foundations/customer/serve/search;
因此我们把Controller代码写道customer包下便完成了修复;思考一下这是为什么呢,为什么会报这个错误?
当前端拿着/foundations/customer/serve/search这个路径后以get方式发送请求,请求来到/foundations/customer/serve下查看我已有的几个Get接口:
然后请求就把“search”这一路径名当成了findById接口方法的参数并传进去,于是就变成了search成为参数且是String类型的,而我们的参数列表中却是Long类型的,于是报了这个错误
三、预约下单
在服务详情页面,点击"立即预约"就可以进行服务的下单操作

在订单系统设计时,通常有两张表:订单表和订单明细表
-
订单表:记录订单号、订单总金额、下单人、订单状态等信息
-
订单明细表:记录购买商品的信息,包括:商品名称、商品价格、购买商品数量等
由于一个订单可以包含多个订单明细,因此二者具有一对多的关系

订单表与订单明细表关系设计大体如下

但是如果系统需求是一个订单只包括一种商品,此时无须记录订单明细,直接将明细合到订单表即可
整个订单模块包括:订单管理、抢单、派单、历史订单四个小模块,对应的工程如下:

我们今天主要实现的是订单管理功能,所以共创建两个工程:jzo2o-orders-base和jzo2o-orders-manager
找到资料中的jzo2o-orders.zip解压之后导入到IDEA中即可,工程结构如下:

请求路径:POST /orders-manager/consumer/orders/place
请求、响应参数:

ConsumerOrdersController:
@Autowired
private IOrdersCreateService ordersCreateService;@ApiOperation("下单接口")
@PostMapping("/place")
public PlaceOrderResDTO place(@RequestBody PlaceOrderReqDTO placeOrderReqDTO) {return ordersCreateService.placeOrder(placeOrderReqDTO);
}
IOrdersCreateService:
/*** 创建订单** @param placeOrderReqDTO 订单参数* @return 订单id*/
PlaceOrderResDTO placeOrder(PlaceOrderReqDTO placeOrderReqDTO);
OrdersCreateServiceImpl:
@Autowired
private RedisTemplate redisTemplate;@Autowired
private ServeApi serveApi;@Autowired
private AddressBookApi addressBookApi;@Override
public PlaceOrderResDTO placeOrder(PlaceOrderReqDTO placeOrderReqDTO) {//1. 调用运营微服务, 根据服务id查询ServeAggregationResDTO serveDto = serveApi.findById(placeOrderReqDTO.getServeId());if (ObjectUtil.isNull(serveDto) || serveDto.getSaleStatus() != 2) {throw new ForbiddenOperationException("服务不存在或者状态有误");}//2. 调用customer微服务, 根据地址id查询信息AddressBookResDTO addressDto = addressBookApi.detail(placeOrderReqDTO.getAddressBookId());if (ObjectUtil.isNull(addressDto)) {throw new ForbiddenOperationException("服务地址有误");}//3. 准备Orders实体类对象Orders orders = new Orders();orders.setId(generateOrderId());//订单idorders.setUserId(UserContext.currentUserId());//下单人idorders.setServeId(placeOrderReqDTO.getServeId());//服务id//运营数据微服务orders.setServeTypeId(serveDto.getServeTypeId());//服务类型idorders.setServeTypeName(serveDto.getServeTypeName());//服务类型名称orders.setServeItemId(serveDto.getServeItemId());//服务项idorders.setServeItemName(serveDto.getServeItemName());//服务项名称orders.setServeItemImg(serveDto.getServeItemImg());//服务项图片orders.setUnit(serveDto.getUnit());//服务单位orders.setPrice(serveDto.getPrice());//服务单价orders.setCityCode(serveDto.getCityCode());//城市编码orders.setOrdersStatus(0);//订单状态: 待支付orders.setPayStatus(2);//支付状态: 待支付orders.setPurNum(placeOrderReqDTO.getPurNum());//购买数量orders.setTotalAmount(serveDto.getPrice().multiply(new BigDecimal(placeOrderReqDTO.getPurNum())));//总金额: 价格 * 购买数量orders.setDiscountAmount(new BigDecimal(0));//优惠金额orders.setRealPayAmount(orders.getTotalAmount().subtract(orders.getDiscountAmount()));//实付金额 订单总金额 - 优惠金额//地址orders.setServeAddress(addressDto.getAddress());//服务详细地址orders.setContactsPhone(addressDto.getPhone());//联系人手机号orders.setContactsName(addressDto.getName());//联系人名字orders.setLon(addressDto.getLon());//经度orders.setLat(addressDto.getLat());//纬度orders.setServeStartTime(placeOrderReqDTO.getServeStartTime());//服务开始时间orders.setDisplay(1);//用户端是否展示 1 展示orders.setSortBy(DateUtils.toEpochMilli(placeOrderReqDTO.getServeStartTime()) + orders.getId() % 100000);//排序字段//4. 保存到数据表this.save(orders);//5.返回return new PlaceOrderResDTO(orders.getId());
}/*** 生成订单id** @return 订单id 19位:2位年+2位月+2位日+13位序号(自增)*/
private Long generateOrderId() {//1. 2位年+2位月+2位日Long yyMMdd = DateUtils.getFormatDate(LocalDateTime.now(), "yyMMdd");//2. 自增数字 1 2Long num = redisTemplate.opsForValue().increment(RedisConstants.Lock.ORDERS_SHARD_KEY_ID_GENERATOR, 1);//1 代表的是每次增长量为1//3. 组装返回return yyMMdd * 10000000000000L + num;
}
可以通过Alt+Enter键选择Generate all setter no default value快速生成类中需要的字段
远程接口:
1.根据地址簿Id远程调用客户中心,查询我的地址簿信息
请求路径:GET /customer/inner/address-book/{id}
请求、响应参数:

在jzo2o-customer中创建com.jzo2o.customer.controller.inner.InnerAddressBookController:
package com.jzo2o.customer.controller.inner;import cn.hutool.core.bean.BeanUtil;
import com.jzo2o.api.customer.AddressBookApi;
import com.jzo2o.api.customer.CommonUserApi;
import com.jzo2o.api.customer.dto.response.AddressBookResDTO;
import com.jzo2o.api.customer.dto.response.CommonUserResDTO;
import com.jzo2o.common.utils.BeanUtils;
import com.jzo2o.customer.model.domain.AddressBook;
import com.jzo2o.customer.service.IAddressBookService;
import com.jzo2o.customer.service.ICommonUserService;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@RestController
@RequestMapping("inner/address-book")
@Api(tags = "内部接口 - 普通用户地址簿相关接口")
public class InnerAddressBookController implements AddressBookApi {@Autowiredprivate IAddressBookService addressBookService;/*** 根据地址簿ID获取地址详情信息*/@GetMapping("/{id}")@Overridepublic AddressBookResDTO detail(@PathVariable("id") Long id) {AddressBook addressBook = addressBookService.getById(id);return BeanUtil.copyProperties(addressBook, AddressBookResDTO.class);}
}
2.根据服务Id远程调用运营基础服务,查询服务相关的信息
在jzo2o-foundations中创建com.jzo2o.foundations.controller.inner.InnerServeController:
package com.jzo2o.foundations.controller.inner;import com.jzo2o.api.foundations.dto.response.ServeAggregationResDTO;
import com.jzo2o.foundations.service.IServeService;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;//内部接口 - 服务相关接口
@RestController
@RequestMapping(value = "/inner/serve")
@ApiOperation("内部接口 - 服务相关接口")
public class InnerServeController {@Resourceprivate IServeService serveService;//根据ID查询服务详情@GetMapping("/{id}")public ServeAggregationResDTO findById(@PathVariable("id")Long id){return serveService.findServeDetailById(id);}
}
IServeService:
/*** 根据ID查询服务详情** @param id 服务id* @return 服务详情*/
ServeAggregationResDTO findServeDetailById(Long id);
ServeServiceImpl:
@Override
public ServeAggregationResDTO findServeDetailById(Long id) {return baseMapper.findServeDetailById(id);
}
ServeMapper:
/*** 根据ID查询服务详情** @param id 服务id* @return 服务详情*/
ServeAggregationResDTO findServeDetailById(Long id);
ServeMapper.xml:
<select id="findServeDetailById" resultType="com.jzo2o.api.foundations.dto.response.ServeAggregationResDTO">SELECTs.id,s.city_code,s.price,s.is_hot,s.hot_time_stamp,s.sale_status,si.id AS serve_item_id,si.`name` AS serve_item_name,si.img AS serve_item_img,si.detail_img,si.serve_item_icon,si.unit,si.sort_num AS serve_item_sort_num,si.serve_type_id AS serve_type_id,st.`name` AS serve_type_name,st.img AS serve_type_img,st.serve_type_icon,st.sort_num AS serve_type_sort_numFROMserve sinner JOIN serve_item si ON si.id = s.serve_item_idinner JOIN serve_type st ON st.id = si.serve_type_idWHEREs.id = #{id}</select>
效果展示:


关于事务失效:
观察当前下单的方法,如果我们想对这个方法添加事务控制,应该如何做呢?
按照Spring声明式事务的控制方式,只需要在placeOrder方法上添加一个事务注解@Transactional即可

这样的话,整个方法就处于事务控制之下,Spring会在进入方法的时候开事务,离开方法的时候提交或者回滚事务
仔细观察会发现,整个事务的逻辑还是比较长的,也就意味着事务的时间比较长,对数据库性能是有影响的
那么我们能不能只对其中的一段代码添加事务呢,比方只对其中的this.save(orders);一行添加事务?
如果想实现的话有两种方案:
-
使用Spring的编程式事务,这种方式控制粒度更精准,但是缺点是代码耦合度高,需要自己写代码处理事务
-
将需要控制事务的那段代码单独提到一个方法中去,然后仅仅对新抽取的方法控制事务
@Override
public PlaceOrderResDTO placeOrder(PlaceOrderReqDTO placeOrderReqDTO) {//... 省略代码this.saveOrders(orders);//... 省略代码
}//新编写一个方法,将需要事务控制事务的方法,提到这个位置来
@Transactional
public void saveOrders(Orders orders){this.save(orders);
}
上面的方法可以很好的解决长事务的问题,但是它也引入了一个新的问题,那就是事务失效
@Transactional
public void saveOrders(Orders orders){this.save(orders);//手动模拟一个异常,看看事务会不会回滚int i = 1 / 0;
}
运行上面的代码,会发现事务不再自动回滚;
这是由于Spring事务的原理是用代理对象实现的,我们通过断点的方式发现此时this并不是代理对象而是OrdersCreateServiceImpl实现类对象;
我们可以通过注入代理对象的方式手动让代理对象调用事务方法:
@Autowired
private IOrdersCreateService owner; @Override
public PlaceOrderResDTO placeOrder(PlaceOrderReqDTO placeOrderReqDTO) {//... 省略代码owner.saveOrders(orders);//... 省略代码
}@Transactional
public void saveOrders(Orders orders){this.save(orders);//模拟异常//int i = 1 / 0;
}
接口中也需要对应的暴露新增的方法
/*** 保存订单** @param orders 订单*/
void saveOrders(Orders orders);
事务失效的情况有哪些?
-
非事务方法内部调用事务方法
-
事务方法没有使用public修饰
-
事务方法的异常在方法内被捕获处理
-
事务方法抛出的异常与rollbackFor属性指定的异常不匹配
-
事务方法配置的事务传播行为有误
① 非事务方法内部调用事务方法
在下面方法中,insertOrderAndReduceStock()方法使用的是原始对象调用的,而不是代理对象调用的
而事务管理功能是代理对象负责的,因此事务失效!
@Service
public class OrderService {public void createOrder(){// ... 准备订单数据// 生成订单并扣减库存insertOrderAndReduceStock();}@Transactionalpublic void insertOrderAndReduceStock(){// 生成订单insertOrder();// 扣减库存reduceStock();}
}
② 事务方法没有使用public修饰
Spring的声明式事务是基于AOP方式结合动态代理来实现的,在其内部有一个类
AbstractFallbackTransactionAttributeSource会检查方法是否使用public修饰,如果不是,则不能进行功能增强
在下面方法中,createOrder()方法没有使用public修饰,因此无法进行事务增强,事务失效!
@Service
public class OrderService {@Transactionalprivate void createOrder(){// ... 准备订单数据// 生成订单insertOrder();// 扣减库存reduceStock();}
}
③ 事务方法的异常在方法内被捕获处理
在下面方法中,createOrder方法执行过程中即便出现了异常也不会向外抛出
而Spring的事务管理就是要感知业务方法的异常,当捕获到异常后才会回滚事务
现在事务被捕获,就会导致Spring无法感知事务异常,自然不会回滚,事务就失效了!
@Service
public class OrderService {@Transactionalprivate void createOrder(){// ... 准备订单数据try {// 生成订单insertOrder();// 扣减库存reduceStock();} catch (Exception e) {// 处理异常}}
}
④ 事务方法抛出的异常与rollbackFor属性指定的异常不匹配
Spring的@Transactional使用rollbackFor属性指定当前事务管理器感知的异常类型(默认为RuntimeException)
下面方法createOrder方法抛出了一个IOException,不会被Spring捕获,事务就失效了!
@Servicepublic class OrderService {@Transactional(rollbackFor = RuntimeException.class)public void createOrder() throws IOException {// ... 准备订单数据try {// 生成订单insertOrder();// 扣减库存reduceStock();} catch (Exception e) {// 处理异常throw new IOException();}}}
⑤ 事务方法配置的事务传播行为有误
下面方法reduceStock方法配置了事务传播行为Propagation.REQUIRES_NEW,他代表当前方法会开启一个新事物
也就是与createOrder和insertOrder不在同一个事物中了,因此事务失效!
@Service
public class OrderService {@Transactionalpublic void createOrder() {// 生成订单insertOrder();// 扣减库存reduceStock();}@Transactionalpublic void insertOrder() {}//Propagation.REQUIRES_NEW 代表必须新事物@Transactional(propagation = Propagation.REQUIRES_NEW)public void reduceStock() {}
}

