通过策略+工厂模式来重构多个版本的下单流程
看到这里相信大家对用户生成订单的流程有了详细的了解了,针对锁的分类、锁的粒度、同步转异步的优化等分别写出了4个生成订单的版本,而且用于分布式锁使用了切面的方式,在Controller和Servic层中添加了Lock层,来实现切面的锁
当写到这里发现Lock层设计的有点冗余,最好是去掉,还有就是生成订单的版本是多个,如果后续再加新版本的话,类与类之间的单一职责原则和开闭原则就不是那么符合了
所以经过上述原因,将生成订单的多个版本流程进行重构,将Lock层去掉,使用策略+工厂模式来优化下单流程。
本次优化是在Controller和Lock层进行,Service层并不受到影响,不影响大家学习之前讲解完的业务。
策略模式
这4个版本的生成订单流程其实就是对应着不同的生成订单策略,所以这种场景使用策略模式再适合不过了,那么既然是策略,我们就要定义一个生成订单策略的接口,来规定生成订单的行为
生成订单策略接口
public interface ProgramOrderStrategy {
/**
* 创建订单
* @param programOrderCreateDto 订单参数
* @return 订单编号
* */
String createOrder(ProgramOrderCreateDto programOrderCreateDto);
/**
* 获取版本号
* @return 版本号
* */
String version();
}当定义好生成订单策略的接口后,就开始实现不同版本生成订单的具体策略了,就是将之前Lock层的生成订单逻辑移动到各自版本的生成订单策略中
已生成订单版本1为例:
@Component
public class ProgramOrderV1Strategy implements ProgramOrderStrategy {
@Autowired
private ProgramOrderService programOrderService;
@Autowired
private CompositeContainer compositeContainer;
@RepeatExecuteLimit(
name = RepeatExecuteLimitConstants.CREATE_PROGRAM_ORDER,
keys = {"#programOrderCreateDto.userId","#programOrderCreateDto.programId"})
@ServiceLock(name = PROGRAM_ORDER_CREATE_V1,keys = {"#programOrderCreateDto.programId"})
@Override
public String createOrder(final ProgramOrderCreateDto programOrderCreateDto) {
compositeContainer.execute(CompositeCheckType.PROGRAM_ORDER_CREATE_CHECK.getValue(),programOrderCreateDto);
return programOrderService.create(programOrderCreateDto);
}
}当把这四个版本的生成订单策略逻辑都设计好了后,就开始思考如何去调用这些实现好的策略,如果此时我们要具体调用的话,就会写成下面这样:
@Operation(summary = "购票V1")
@PostMapping(value = "/create/v1")
public ApiResponse<String> createV1(@Valid @RequestBody ProgramOrderCreateDto programOrderCreateDto) {
ProgramOrderStrategy programOrderStrategy = SpringUtil.getBean(ProgramOrderV1Strategy.class);
String orderNumber = programOrderStrategy.createOrder(programOrderCreateDto);
return ApiResponse.ok(orderNumber);
}在Controller层的方法中,先获取 ProgramOrderV1Strategy 的bean对象,通过向上转型 ProgramOrderStrategy 类型来接收,然后调用定好的策略方法 createOrder 来生成订单,获取订单编号,最后返回即可。
整个流程是没有问题的,但是总感觉差点意思,不是那么的优化,也就是从Spring获取对象的过程
SpringUtil.getBean(ProgramOrderV1Strategy.class);工厂模式
我们不应该从Spring容器中直接拿取ProgramOrderV1Strategy 具体的实现策略,这样对扩展性并不友好,如果后续改成在ProgramOrderCreateDto入参中加入订单版本参数,前端直接传入版本就能直接调用对应的生成订单版本,目前这种方式就不符合了
所以需要更好的方式来取得具体的策略实现,而什么结构和这种业务符合呢?通过一个参数直接能命中拿到一个具体策略?也就是说通过key能拿到Value?
我都说了通过Key想拿到Value,那自然是Map结构了啊!那么再思考需要线程安全吗?还是思考流程,我们要什么时候往Map中放入策略?那肯定是策略这个对象构建好了,就可以放进入了,而这些策略对象通过通过Spring来管理,而Spring加载对象是线程安全的,所以我们直接使用HashMap就可以了,不需要考虑线程安全问题了
既然我们确定好选用HashMap作为存放策略对象的容器后,那就要再设计一个上下文,来操作这个HashMap
存放生成订单策略实现的上下文
@Component
public class ProgramOrderContext {
private static final Map<String,ProgramOrderStrategy> MAP = new HashMap<>(8);
@Autowired
private List<ProgramOrderStrategy> programOrderStrategyList;
@PostConstruct
public void init() {
for (ProgramOrderStrategy programOrderStrategy : programOrderStrategyList) {
MAP.put(programOrderStrategy.version(), programOrderStrategy);
}
}
public ProgramOrderStrategy get(String version){
return Optional.ofNullable(MAP.get(version)).orElseThrow(() ->
new DaMaiFrameException(BaseCode.PROGRAM_ORDER_STRATEGY_NOT_EXIST));
}
}这个上下文就是所谓的工厂,当我们传入版本参数后,此工厂就能直接给我们想要的对应版本策略。
上下文有了后,下面就来具体思考什么时候往上下文里放了,
具体策略的加载
当项目启动时,programOrderStrategyList 这个就是从 Spring 容器中获取所有 ProgramOrderStrategy 类型的集合,也就是所有实现策略的集合了,
接着执行被 @PostConstruct 修饰的 init 方法,这个方法的逻辑很简单,循环 programOrderStrategyList 这个集合,放到 MAP 中,key 就是版本号,value 就是对应的策略实现了。
版本枚举
在从上下文获取和放入的方法中,版本参数是使用字符串,通过ProgramOrderVersion枚举中的version来指定
public enum ProgramOrderVersion {
/**
* 版本
* */
V1_VERSION("v1","v1版本"),
V2_VERSION("v2","v2版本"),
V3_VERSION("v3","v3版本"),
V4_VERSION("v4","v4版本"),
;
private final String version;
private final String msg;
ProgramOrderVersion(String version, String msg) {
this.version = version;
this.msg = msg;
}
public String getVersion() {
return version;
}
public String getMsg() {
return this.msg == null ? "" : this.msg;
}
public static String getMsg(String version) {
for (ProgramOrderVersion re : ProgramOrderVersion.values()) {
if (re.version.equals(version)) {
return re.msg;
}
}
return "";
}
public static ProgramOrderVersion getRc(String version) {
for (ProgramOrderVersion re : ProgramOrderVersion.values()) {
if (re.version.equals(version)) {
return re;
}
}
return null;
}
}讲解到这里,我们的具体实现策略才真正的成型,下面就将每个具体的实现策略粘贴出来
生成订单v1策略
@Component
public class ProgramOrderV1Strategy implements ProgramOrderStrategy {
@Autowired
private ProgramOrderService programOrderService;
@Autowired
private CompositeContainer compositeContainer;
@RepeatExecuteLimit(
name = RepeatExecuteLimitConstants.CREATE_PROGRAM_ORDER,
keys = {"#programOrderCreateDto.userId","#programOrderCreateDto.programId"})
@ServiceLock(name = PROGRAM_ORDER_CREATE_V1,keys = {"#programOrderCreateDto.programId"})
@Override
public String createOrder(final ProgramOrderCreateDto programOrderCreateDto) {
compositeContainer.execute(CompositeCheckType.PROGRAM_ORDER_CREATE_CHECK.getValue(),programOrderCreateDto);
return programOrderService.create(programOrderCreateDto,ProgramOrderVersion.V1_VERSION.getValue());
}
@Override
public String version() {
return ProgramOrderVersion.V1_VERSION.getVersion();
}
}生成订单v2策略
@Slf4j
@Component
public class ProgramOrderV2Strategy implements ProgramOrderStrategy {
@Autowired
private ProgramOrderService programOrderService;
@Autowired
private ServiceLockTool serviceLockTool;
@Autowired
private CompositeContainer compositeContainer;
@Autowired
private LocalLockCache localLockCache;
@RepeatExecuteLimit(
name = RepeatExecuteLimitConstants.CREATE_PROGRAM_ORDER,
keys = {"#programOrderCreateDto.userId","#programOrderCreateDto.programId"})
@Override
public String createOrder(ProgramOrderCreateDto programOrderCreateDto) {
compositeContainer.execute(CompositeCheckType.PROGRAM_ORDER_CREATE_CHECK.getValue(),programOrderCreateDto);
List<SeatDto> seatDtoList = programOrderCreateDto.getSeatDtoList();
List<Long> ticketCategoryIdList = new ArrayList<>();
if (CollectionUtil.isNotEmpty(seatDtoList)) {
ticketCategoryIdList =
seatDtoList.stream().map(SeatDto::getTicketCategoryId).distinct().sorted().collect(Collectors.toList());
}else {
ticketCategoryIdList.add(programOrderCreateDto.getTicketCategoryId());
}
List<ReentrantLock> localLockList = new ArrayList<>(ticketCategoryIdList.size());
List<RLock> serviceLockList = new ArrayList<>(ticketCategoryIdList.size());
List<ReentrantLock> localLockSuccessList = new ArrayList<>(ticketCategoryIdList.size());
List<RLock> serviceLockSuccessList = new ArrayList<>(ticketCategoryIdList.size());
for (Long ticketCategoryId : ticketCategoryIdList) {
String lockKey = StrUtil.join("-",PROGRAM_ORDER_CREATE_V2,
programOrderCreateDto.getProgramId(),ticketCategoryId);
ReentrantLock localLock = localLockCache.getLock(lockKey,false);
RLock serviceLock = serviceLockTool.getLock(LockType.Reentrant, lockKey);
localLockList.add(localLock);
serviceLockList.add(serviceLock);
}
for (ReentrantLock reentrantLock : localLockList) {
try {
reentrantLock.lock();
}catch (Throwable t) {
break;
}
localLockSuccessList.add(reentrantLock);
}
boolean serviceLockFail = false;
for (RLock rLock : serviceLockList) {
try {
rLock.lock();
}catch (Throwable t) {
serviceLockFail = true;
break;
}
serviceLockSuccessList.add(rLock);
}
try {
if (serviceLockFail) {
throw new DaMaiFrameException(BaseCode.SERVICE_LOCK_FAIL);
}
return programOrderService.create(programOrderCreateDto,ProgramOrderVersion.V2_VERSION.getValue());
}finally {
for (int i = serviceLockSuccessList.size() - 1; i >= 0; i--) {
RLock rLock = serviceLockSuccessList.get(i);
try {
rLock.unlock();
}catch (Throwable t) {
log.error("service lock unlock error",t);
}
}
for (int i = localLockSuccessList.size() - 1; i >= 0; i--) {
ReentrantLock reentrantLock = localLockSuccessList.get(i);
try {
reentrantLock.unlock();
}catch (Throwable t) {
log.error("local lock unlock error",t);
}
}
}
}
@Override
public String version() {
return ProgramOrderVersion.V2_VERSION.getVersion();
}
}生成订单v3策略
@Slf4j
@Component
public class ProgramOrderV3Strategy implements ProgramOrderStrategy {
@Autowired
private ProgramOrderService programOrderService;
@Autowired
private BaseProgramOrder baseProgramOrder;
@Autowired
private CompositeContainer compositeContainer;
@RepeatExecuteLimit(
name = RepeatExecuteLimitConstants.CREATE_PROGRAM_ORDER,
keys = {"#programOrderCreateDto.userId","#programOrderCreateDto.programId"})
@Override
public String createOrder(ProgramOrderCreateDto programOrderCreateDto) {
compositeContainer.execute(CompositeCheckType.PROGRAM_ORDER_CREATE_CHECK.getValue(),programOrderCreateDto);
return baseProgramOrder.localLockCreateOrder(PROGRAM_ORDER_CREATE_V3,programOrderCreateDto,
() -> programOrderService.createNew(programOrderCreateDto,ProgramOrderVersion.V3_VERSION.getValue()));
}
@Override
public String version() {
return ProgramOrderVersion.V3_VERSION.getVersion();
}
}生成订单v4策略
@Slf4j
@Component
public class ProgramOrderV4Strategy implements ProgramOrderStrategy {
@Autowired
private ProgramOrderService programOrderService;
@Autowired
private BaseProgramOrder baseProgramOrder;
@Autowired
private CompositeContainer compositeContainer;
@RepeatExecuteLimit(
name = RepeatExecuteLimitConstants.CREATE_PROGRAM_ORDER,
keys = {"#programOrderCreateDto.userId","#programOrderCreateDto.programId"})
@Override
public String createOrder(ProgramOrderCreateDto programOrderCreateDto) {
compositeContainer.execute(CompositeCheckType.PROGRAM_ORDER_CREATE_CHECK.getValue(),programOrderCreateDto);
return baseProgramOrder.localLockCreateOrder(PROGRAM_ORDER_CREATE_V4,programOrderCreateDto,
() -> programOrderService.createNewAsync(programOrderCreateDto,ProgramOrderVersion.V4_VERSION.getValue()));
}
@Override
public String version() {
return ProgramOrderVersion.V4_VERSION.getVersion();
}
}BaseProgramOrder
由于v3和v4版本还都需要本地锁的逻辑,所以把本地锁的逻辑放到了BaseProgramOrder中,这样v3和v4的实现策略可以实现共用
@Slf4j
@Component
public class BaseProgramOrder {
@Autowired
private LocalLockCache localLockCache;
public String localLockCreateOrder(String lockKeyPrefix, ProgramOrderCreateDto programOrderCreateDto,
LockTask<String> lockTask){
List<SeatDto> seatDtoList = programOrderCreateDto.getSeatDtoList();
List<Long> ticketCategoryIdList = new ArrayList<>();
if (CollectionUtil.isNotEmpty(seatDtoList)) {
ticketCategoryIdList =
seatDtoList.stream().map(SeatDto::getTicketCategoryId).distinct().sorted().collect(Collectors.toList());
}else {
ticketCategoryIdList.add(programOrderCreateDto.getTicketCategoryId());
}
List<ReentrantLock> localLockList = new ArrayList<>(ticketCategoryIdList.size());
List<ReentrantLock> localLockSuccessList = new ArrayList<>(ticketCategoryIdList.size());
for (Long ticketCategoryId : ticketCategoryIdList) {
String lockKey = StrUtil.join("-",lockKeyPrefix,
programOrderCreateDto.getProgramId(),ticketCategoryId);
ReentrantLock localLock = localLockCache.getLock(lockKey,false);
localLockList.add(localLock);
}
for (ReentrantLock reentrantLock : localLockList) {
try {
reentrantLock.lock();
}catch (Throwable t) {
break;
}
localLockSuccessList.add(reentrantLock);
}
try {
return lockTask.execute();
}finally {
for (int i = localLockSuccessList.size() - 1; i >= 0; i--) {
ReentrantLock reentrantLock = localLockSuccessList.get(i);
try {
reentrantLock.unlock();
}catch (Throwable t) {
log.error("local lock unlock error",t);
}
}
}
}
}调用流程
在Controller层中,我们就可以直接通过传入版本来获取对应的策略,然后调用生成订单方法即可。
@RestController
@RequestMapping("/program/order")
@Tag(name = "program-order", description = "节目订单")
public class ProgramOrderController {
@Autowired
private ProgramOrderContext programOrderContext;
@Operation(summary = "购票V1")
@PostMapping(value = "/create/v1")
public ApiResponse<String> createV1(@Valid @RequestBody ProgramOrderCreateDto programOrderCreateDto) {
return ApiResponse.ok(programOrderContext.get(ProgramOrderVersion.V1_VERSION.getVersion())
.createOrder(programOrderCreateDto));
}
@Operation(summary = "购票V2")
@PostMapping(value = "/create/v2")
public ApiResponse<String> createV2(@Valid @RequestBody ProgramOrderCreateDto programOrderCreateDto) {
return ApiResponse.ok(programOrderContext.get(ProgramOrderVersion.V2_VERSION.getVersion())
.createOrder(programOrderCreateDto));
}
@Operation(summary = "购票V3")
@PostMapping(value = "/create/v3")
public ApiResponse<String> createV3(@Valid @RequestBody ProgramOrderCreateDto programOrderCreateDto) {
return ApiResponse.ok(programOrderContext.get(ProgramOrderVersion.V3_VERSION.getVersion())
.createOrder(programOrderCreateDto));
}
@Operation(summary = "购票V4")
@PostMapping(value = "/create/v4")
public ApiResponse<String> createV4(@Valid @RequestBody ProgramOrderCreateDto programOrderCreateDto) {
return ApiResponse.ok(programOrderContext.get(ProgramOrderVersion.V4_VERSION.getVersion())
.createOrder(programOrderCreateDto));
}
}以生成订单v1版本为例,在生成订单方法中,通过ProgramOrderVersion.V1_VERSION.getVersion()来获取版本,然后将此版本传入策略上下文中,就可以获取具体的策略了。
这种方式比最开始将的从Spring容器中直接获取具体实现要好多了,不过还是没有真正的让前端改成通过传递版本参数来自动匹配,原因是大家到这里已经将整个流程都学习的差不多了,动前端逻辑修改地方比较多,重点还是想让小伙伴掌握这个核心技巧。
本人将后端自动匹配版本的逻辑贴在下面,其实特别简单,大家直接看就可以了
入参
@Data
@Schema(title="ProgramOrderCreateDto", description ="节目订单创建")
public class ProgramOrderCreateDto {
@Schema(name ="programId", type ="Long", description ="节目id",requiredMode= RequiredMode.REQUIRED)
@NotNull
private Long programId;
@Schema(name ="userId", type ="Long", description ="用户id",requiredMode= RequiredMode.REQUIRED)
@NotNull
private Long userId;
@Schema(name ="ticketUserIdList", type ="List<Long>", description ="购票人id集合",requiredMode= RequiredMode.REQUIRED)
@NotNull
private List<Long> ticketUserIdList;
@Schema(name ="seatDtoList", type ="List<SeatDto>", description = "座位")
private List<SeatDto> seatDtoList;
@Schema(name ="ticketCategoryId", type ="Long", description = "节目票档id(如果不选座位,那么票档id必填)")
private Long ticketCategoryId;
@Schema(name ="ticketCount", type ="Integer", description = "购买票数量(如果不选座位,那么购买票数量必填)")
private Integer ticketCount;
/**
* 订单版本参数
*/
@Schema(name ="createOrderVersion", type ="String", description ="生成订单的版本",requiredMode= RequiredMode.REQUIRED)
@NotBlank
private String createOrderVersion;
}更新: 2026-01-22 09:32:21
原文: https://www.yuque.com/u22210564/ykdrdh/sc4unob6or8tv4wx