当前位置: 首页 > news >正文

搜索下单(es、mysql、MQ同步;关于事务失效)

目录

搜索下单:

一、环境配置

1.MySQL配置:

2.RabbitMQ配置:

3.Canal配置:

测试MySQL-Canal-MQ

4.ES配置:

代码讲解:

管理同步表:

1)区域服务上架

2)区域服务下架

二、搜索服务:

1)需求分析

2)代码开发

三、预约下单

远程接口:

关于事务失效:

事务失效的情况有哪些?


搜索下单:

在当前项目中,有两处使用到了搜索服务:

  1. 门户最上端的搜索框(根据关键词搜索服务信息)

  2. 全部服务界面,点击分类名称搜索分类下的服务项目(根据服务分类id搜索服务项目)

如果要实现此搜索功能,ES肯定是第一选择,因为单纯的数据库查询从能力和性能上都无法满足需求

如果要从ES中进行数据的检索,就必须保证数据库中的参与搜索的这部分数据实时同步到ES中

此功能的实现大体有下面这些方案:

  • 同步双写:在程序在中同时向MySQL和ES写数据,这种方式实现简单,但是代码耦合

  • 异步消息:程序在向MySQL写入数据之后,向MQ中投递消息,ES相关程序监听MQ,获取数据写入ES

  • Canal监听:使用Canal监听MySQL的binlog,当发现写入操作后,立即读取到新写入的内容,并同步到ES

Canal的工作原理

  1. Canal伪装自己为MySQL的从节点,向MySQL主节点发送dump协议

  2. MySQL主节点一旦收到dump请求,开始推送binlog给canal

  3. Canal会接收并解析这些变更事件并解析binlog,并发送到其它服务器(比如es、redis等等)

我们项目中采用的是将后两种方案相结合的方式,也就是使用Canal+MQ来保证MySQL跟ES中数据的一致性,具体思路如下:

  1. 运营人员对服务信息相关表(serve、serve_item、serve_type)进行增删改操作,MySQL记录binlog日志

  2. Canal读取binlog解析出增加、修改、删除数据的记录,然后发送到MQ中

  3. 同步程序监听MQ,收到增加、修改、删除数据的记录,请求ES创建、修改、删除索引数据

  4. 小程序端用户请求服务搜索接口从ES中搜索服务信息

这里为了方便数据同步,我们单独设计了一张数据表serve_sync用来保存所有需要同步的数据

也就是说当我们对serve、serve_type、serve_item中相关字段进行操作的时候,也要同步修改一下serve_sync表

这样一来,我们就只需要将serve_sync中的数据同步到es中就可以了

总结一下我们要做的事情:

  1. 保证从MySQL-Canal-MQ-ES这套环境是没问题的

  2. 保证serve、serve_item、serve_type表数据变化的时候,serve_sync表中的数据要同步变化

  3. 保证小程序端用户可以从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);}
}

效果展示:

代码讲解:

模版方法设计模式

适用场景:

一个功能的完成需要一系列的步骤,这些步骤是固定的,但是某些步骤的具体实现是待定的

比如:老师和学生在上午的经历步骤是一样的,都是吃早饭、工作、吃午饭

但是其中工作的内容是不一样的,老师的工作是讲课,学生的工作是学习

实现流程:

  1. 定义一个抽象类作为父类,在父类中提供模版方法

  2. 在模板方法中定义出流程的步骤,能确定的行为直接写出,不能确定的行为定义为抽象方法

  3. 根据不同的行为定义不同的子类,继承抽象类,重写父类的抽象方法,完成各自的行为内容

        由于解析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-basejzo2o-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);一行添加事务?

如果想实现的话有两种方案:

  1. 使用Spring的编程式事务,这种方式控制粒度更精准,但是缺点是代码耦合度高,需要自己写代码处理事务

  2. 将需要控制事务的那段代码单独提到一个方法中去,然后仅仅对新抽取的方法控制事务

@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() {}
}

http://www.dtcms.com/a/618098.html

相关文章:

  • aleph-node Node upgrade instructions 节点升级说明
  • 找谁做网站网站建设与运营培训班
  • 智能制造与工业4.0:5G与物联网的深度融合
  • GSV1201S(2201S)#ACP@支持 DisplayPort 1.2 到 HDMI 1.4 转换且集成嵌入式 MCU
  • Linux SPI 驱动实验
  • 【开题答辩全过程】以 基于Java的水族馆销售与经营管理系统的设计与实现为例,包含答辩的问题和答案
  • 网站响应是什么问题吗最近国际时事
  • 从拟南芥到线虫:我的生物信息学多组学实操笔记
  • 指尖革命!2025输入法生态位深度测评:智能,远不止于输入!
  • 如何在Windows系统上安装和配置Node.js及Node版本管理器(nvm)
  • 网站开发 保证书旅游网站制作分析
  • Docker实战深度解析:从Nginx部署到私有镜像仓库管理
  • 读书笔记|算法的破坏性影响
  • 网站制作需要平台企业网站的切片怎么做
  • 数据结构与算法工程笔记:决策树/sstable与性能优化
  • Linux 服务器配置 rootless docker Quick Start
  • LTE/5G L3 RRC层技术介绍
  • C语言进阶知识--文件操作
  • 网站建设需要哪些资料扶贫网站建设方案
  • C++标准模板库(STL)——list的模拟实现
  • 幽冥大陆(二十二)dark语言智慧农业电子秤读取——东方仙盟炼气期
  • 5.驱动led灯
  • RTL8367RB的国产P2P替代方案用JL6107-PC的可行性及实现方法
  • <MySQL——L1>
  • 有没有网站做字体变形自学软装设计该怎么入手
  • 做网站咸阳贵州省建设厅城乡建设网站
  • 分布式监控Skywalking安装及使用教程(保姆级教程)
  • 可信数据空间的分布式数字凭证和分布式数字身份
  • 分布式WEB应用中会话管理的变迁之路
  • 徐州市建设局招投标网站河南网站建站推广