学习目标
1、掌握redisson分布式对象API
2、掌握redisson分布式集合API
3、掌握redisson分布式锁API
4、完成桌台是否开桌功能
5、完成主体信息查询功能
6、复述出用户开桌操作流程、理解开桌加锁意义
7、完成菜品信息详情功能
8、理解桌台订单信息中购物车订单项、可核算订单项意义
第一章 redisson框架
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务
Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
1、redisson快速入门
这里我们使用spring-boot集成redission,首先需要在pom.xml文件中添加依赖
<properties>
<redisson-spring-boot>3.11.2</redisson-spring-boot>
</properties>
<!--redis缓存客户端-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson-spring-boot}</version>
</dependency>
在项目的resources目录中application.yml添加
spring:
redis:
redisson:
#配置文件目录
config: classpath:singleServerConfig.yaml
#config: classpath:clusterServersConfig.yaml
1.1、Single节点配置
配置单节点模式可以通过在resources目录中指定一个YAML格式的文件来实现。以下是YAML格式的配置文件样本。文件中的字段名称必须与singleServerConfig和config对象里的字段名称相符
---
singleServerConfig:
#如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,
#那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。
#默认值:10000
idleConnectionTimeout: 10000
pingTimeout: 1000
#同任何节点建立连接时的等待超时。时间单位是毫秒。
#默认值:10000
connectTimeout: 10000
#等待节点回复命令的时间。该时间从命令发送成功时开始计时。
#默认值:3000
timeout: 3000
#如果尝试达到 retryAttempts(命令失败重试次数)
#仍然不能将命令发送至某个指定的节点时,将抛出错误。如果尝试在此限制之内发送成功,
#则开始启用 timeout(命令等待超时) 计时
#默认值:3
retryAttempts: 3
#在某个节点执行相同或不同命令时,连续失败failedAttempts(执行失败最大次数)时,
#该节点将被从可用节点列表里清除,直到 reconnectionTimeout(重新连接时间间隔) 超时以后再次尝试。
#默认值:1500
retryInterval: 1500
#重新连接时间间隔
reconnectionTimeout: 3000
#执行失败最大次数
failedAttempts: 3
#密码
password: null
#每个连接的最大订阅数量。
#默认值:5
subscriptionsPerConnection: 5
#在Redis节点里显示的客户端名称。
clientName: null
#在Redis节点
address: "redis://192.168.112.77:6379"
#从节点发布和订阅连接的最小空闲连接数
#默认值:1
subscriptionConnectionMinimumIdleSize: 1
#用于发布和订阅连接的连接池最大容量。连接池的连接数量自动弹性伸缩。
#默认值:50
subscriptionConnectionPoolSize: 50
#节点最小空闲连接数
#默认值:32
connectionMinimumIdleSize: 32
#节点连接池大小
#默认值:64
connectionPoolSize: 64
#这个线程池数量被所有RTopic对象监听器,RRemoteService调用者和RExecutorService任务共同共享。
#默认值: 当前处理核数量 * 2
threads: 8
#这个线程池数量是在一个Redisson实例内,被其创建的所有分布式数据类型和服务,
#以及底层客户端所一同共享的线程池里保存的线程数量。
#默认值: 当前处理核数量 * 2
nettyThreads: 8
#Redisson的对象编码类是用于将对象进行序列化和反序列化,以实现对该对象在Redis里的读取和存储。
#默认值: org.redisson.codec.JsonJacksonCodec
codec: !<org.redisson.codec.JsonJacksonCodec> {}
#传输模式
#默认值:TransportMode.NIO
transportMode: "NIO"
1.2、Cluster节点配置
配置集群模式可以通过指定一个YAML格式的文件来实现。以下是YAML格式的配置文件样本。文件中的字段名称必须与clusterServersConfig和config对象里的字段名称相符。
---
clusterServersConfig:
#如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,
#那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。
#默认值:10000
idleConnectionTimeout: 10000
#同任何节点建立连接时的等待超时。时间单位是毫秒。
#默认值:10000
connectTimeout: 10000
#等待节点回复命令的时间。该时间从命令发送成功时开始计时。
#默认值:3000
timeout: 3000
#如果尝试达到 retryAttempts(命令失败重试次数)
#仍然不能将命令发送至某个指定的节点时,将抛出错误。如果尝试在此限制之内发送成功,
#则开始启用 timeout(命令等待超时) 计时。
#默认值:3
retryAttempts: 3
#在某个节点执行相同或不同命令时,连续失败failedAttempts(执行失败最大次数)时,
#该节点将被从可用节点列表里清除,直到 reconnectionTimeout(重新连接时间间隔) 超时以后再次尝试。
#默认值:1500
retryInterval: 1500
#密码
password: pass
#每个连接的最大订阅数量。
#默认值:5
subscriptionsPerConnection: 5
clientName: null
#负载均衡算法类的选择
#默认值: org.redisson.connection.balancer.RoundRobinLoadBalancer
#在使用多个Elasticache Redis服务节点的环境里,可以选用以下几种负载均衡方式选择一个节点:
#org.redisson.connection.balancer.WeightedRoundRobinBalancer - 权重轮询调度算法
#org.redisson.connection.balancer.RoundRobinLoadBalancer - 轮询调度算法
#org.redisson.connection.balancer.RandomLoadBalancer - 随机调度算法
loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
slaveSubscriptionConnectionMinimumIdleSize: 1
slaveSubscriptionConnectionPoolSize: 50
slaveConnectionMinimumIdleSize: 32
slaveConnectionPoolSize: 64
masterConnectionMinimumIdleSize: 32
masterConnectionPoolSize: 64
readMode: "SLAVE"
nodeAddresses:
- "redis://192.168.112.71:7001"
- "redis://192.168.112.72:7001"
- "redis://192.168.112.73:7001"
- "redis://192.168.112.74:7001"
- "redis://192.168.112.75:7001"
- "redis://192.168.112.76:7001"
scanInterval: 1000
threads: 8
nettyThreads: 8
codec: !<org.redisson.codec.JsonJacksonCodec> {}
"transportMode":"NIO"
2、redisson-分布式对象
上面我们集成了redission客户端,下面我们来看下他的基本使用
2.1、key操作相关
所有与Redis key相关的操作都归纳在RKeys,我们可以通过RedissonClient直接对keys进行操作。
package com.itheima.redission.service;
import lombok.extern.log4j.Log4j2;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBucket;
import org.redisson.api.RKeys;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @ClassName KeyOpertions.java
* @Description 关于key的操作
*/
@Slf4j
@Component
public class KeyOpertions {
@Autowired
RedissonClient redissonClient;
public void foundedKeys(){
RBucket<Object> bucket = redissonClient.getBucket("security:aa");
bucket.set("张三");
//获得所有keys
RKeys keys = redissonClient.getKeys();
Iterable<String> keysAll = keys.getKeys();
for (String key : keysAll) {
log.info("获得key:"+key);
}
//获得所有security开头的key
Iterable<String> redisKeys = keys.getKeysByPattern("security*");
for (String key : redisKeys) {
log.info("获得key:"+key);
}
}
public void deleteKeys(){
RBucket<String> testA = redissonClient.getBucket("testA");
RBucket<String> testB = redissonClient.getBucket("testB");
testA.set("张成成");
testB.set("张成成女朋友");
//获得所有keys
RKeys keys = redissonClient.getKeys();
long flag = keys.delete("testA","testB");
log.info("批量删除key:testA,testB:{}",flag);
testA.set("张成成");
testB.set("张成成女朋友");
flag = keys.deleteByPattern("test*");
log.info("模糊删除key:testA,testB:{}",flag);
}
}
2.2、通用对象桶
Redisson的分布式RBucket是一种通用对象桶可以用来存放任类型的对象,每个Redisson对象实例都会有一个与之对应的Redis数据实例,可以通过调用get***方法来取得Redis数据实例的名称(key),查看BucketOpertions类中的方法BucketOper:
package com.itheima.redission.service;
import com.itheima.redission.pojo.AnyObject;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBucket;
import org.redisson.api.RBuckets;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @ClassName BucketOpertions.java
* @Description 通用对象桶
*/
@Slf4j
@Component
public class BucketOpertions {
@Autowired
RedissonClient redissonClient;
/***
* @description Bucket通用对象通
*/
public void bucketOper(){
//获得Bucket
RBucket<AnyObject> anyObjectRBucket = redissonClient.getBucket("BucketOpertions");
//放入一个元素
AnyObject anyObject = AnyObject.builder()
.name("张三")
.age(19)
.address("中国上海")
.build();
//为BucketOperTest添加元素到redis中
anyObjectRBucket.set(anyObject);
long timeToLive = anyObjectRBucket.remainTimeToLive();
log.info("BucketOperTest存活时间:{}",timeToLive);
//修改BucketOperTest的存活时间为600秒
anyObjectRBucket.set(anyObject,600, TimeUnit.SECONDS);
timeToLive = anyObjectRBucket.remainTimeToLive();
log.info("BucketOperTest存活时间:{}",timeToLive/1000);
//试着为BucketOperTest添加元素到redis中,并且存活时间为600秒
boolean trySetFlag = anyObjectRBucket.trySet(anyObject, 600, TimeUnit.SECONDS);
log.info("试着为BucketOperTest存储元素:{}",trySetFlag);
//在holder中获取当前元素并将其替换为新值
AnyObject anyObjectNew = AnyObject.builder()
.name("李四")
.age(20)
.address("中国北京")
.build();
AnyObject anyObjectResult = anyObjectRBucket.getAndSet(anyObjectNew);
log.info("BucketOperTest原始值:{}",anyObjectResult);
anyObjectResult = anyObjectRBucket.get();
log.info("BucketOperTest新添值:{}",anyObjectResult);
//移除BucketOperTest
boolean deleteFalg = anyObjectRBucket.delete();
log.info("BucketOperTest删除:{}",deleteFalg);
}
/***
* @description Buckets批量通用对象通
*/
public void bucketsOper(){
//获得Bucket
RBucket<AnyObject> bucketA = redissonClient.getBucket("BucketOpertionsTestA");
//放入一个元素
AnyObject anyObject = AnyObject.builder()
.name("张三")
.age(19)
.address("中国上海")
.build();
//为BucketOperTest添加元素到redis中
bucketA.set(anyObject);
//获得Bucket
RBucket<AnyObject> bucketB = redissonClient.getBucket("BucketOpertionsTestB");
bucketB.set(anyObject);
//获得Buckets
RBuckets buckets = redissonClient.getBuckets();
//这里的兼具map的属性
Map<String, AnyObject> bucketsOperMap = buckets.get("BucketOpertionsTestA", "BucketOpertionsTestB");
log.info("map的元素信息:{}",bucketsOperMap);
//删除所有元素
buckets.delete("BucketOpertionsTestA", "BucketOpertionsTestB");
}
}
还可以通过RBuckets接口实现批量操作多个RBucket对象,查看BucketOpertions类中的方法BucketsOper:
/***
* @description Buckets批量通用对象通
*/
public void BucketsOper(){
//获得Bucket
RBucket<AnyObject> bucketA=redissonClient.getBucket("BucketOperTestA");
//放入一个元素
AnyObject anyObject=AnyObject.builder()
.name("张三")
.age(19)
.address("中国上海")
.build();
//为BucketOperTest添加元素到redis中
bucketA.set(anyObject);
//获得Bucket
RBucket<AnyObject> bucketB=redissonClient.getBucket("BucketOperTestB");
bucketB.set(anyObject);
//获得Buckets
RBuckets buckets=redissonClient.getBuckets();
//这里的兼具map的属性
Map<String, AnyObject> bucketsOperMap=buckets.get("BucketOperTestA", "BucketOperTestB");
log.info("map的元素信息:{}",bucketsOperMap);
//删除所有元素
bucketsOperMap.clear();
}
2.3、原子整长形
Redisson的分布式整长形RAtomicLong与java.util.concurrent.atomic.AtomicLong对象类似
package com.itheima.redission.service;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RAtomicLong;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @ClassName AtomicLongOpertions.java
* @Description 原子整长型操作
*/
@Slf4j
@Component
public class AtomicLongOpertions {
@Autowired
RedissonClient redissonClient;
public void atomicLongOper(){
RAtomicLong atomicLongOper=redissonClient.getAtomicLong("AtomicLongOper");
//添加一个从0开始的元素
atomicLongOper.set(0);
//获得当前元素
long flag=atomicLongOper.get();
log.info("获得当前元素:{}",flag);
//先递增1,然后返回元素
flag=atomicLongOper.incrementAndGet();
log.info("先递增1,然后返回元素:{}",flag);
//先获得元素,再递增1
flag=atomicLongOper.getAndIncrement();
log.info("先获得元素,再递增1",flag);
//获得当前元素
flag=atomicLongOper.get();
log.info("获得当前元素:{}",flag);
//先递减1,然后返回元素
flag=atomicLongOper.decrementAndGet();
log.info("先递减1,然后返回元素:{}",flag);
//先获得元素,再递增1
flag=atomicLongOper.getAndDecrement();
log.info("先获得元素,再递减1",flag);
//获得当前元素
flag=atomicLongOper.get();
log.info("获得当前元素:{}",flag);
//删除元素
boolean delete=atomicLongOper.delete();
log.info("删除当前元素:{}",delete);
//添加并且获得元素,如果想批量递减可以传入负数
flag=atomicLongOper.addAndGet(2);
log.info("添加并且获得元素:{}",flag);
//获得并且删除元素
flag=atomicLongOper.getAndDelete();
log.info("获得并且删除元素:{}",flag);
}
}
2.4、原子双精度浮点
Redisson还提供了分布式原子双精度浮点RAtomicDouble弥补了Java自身的不足
package com.itheima.redission.service;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RAtomicDouble;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @ClassName AtomicDoubleOpertions.java
* @Description 原子浮点型操作
*/
@Slf4j
@Component
public class AtomicDoubleOpertions {
@Autowired
RedissonClient redissonClient;
public void atomicDoubleOper(){
RAtomicDouble atomicDoubleOper=redissonClient.getAtomicDouble("AtomicDoubleOper");
//添加一个从2.0开始的元素
atomicDoubleOper.set(2.0D);
//获得当前元素
double flag=atomicDoubleOper.get();
log.info("获得当前元素:{}",flag);
//先递增1,然后返回元素
flag=atomicDoubleOper.incrementAndGet();
log.info("先递增1,然后返回元素:{}",flag);
//先获得元素,再递增1
flag=atomicDoubleOper.getAndIncrement();
log.info("先获得元素,再递增1",flag);
//获得当前元素
flag=atomicDoubleOper.get();
log.info("获得当前元素:{}",flag);
//先递减1,然后返回元素
flag=atomicDoubleOper.decrementAndGet();
log.info("先递减1,然后返回元素:{}",flag);
//先获得元素,再递增1
flag=atomicDoubleOper.getAndDecrement();
log.info("先获得元素,再递减1",flag);
//获得当前元素
flag=atomicDoubleOper.get();
log.info("获得当前元素:{}",flag);
//删除元素
boolean delete=atomicDoubleOper.delete();
log.info("删除当前元素:{}",delete);
//添加并且获得元素
flag=atomicDoubleOper.addAndGet(2);
log.info("添加并且获得元素:{}",flag);
//获得并且删除元素
flag=atomicDoubleOper.getAndDelete();
log.info("获得并且删除元素:{}",flag);
}
}
3、redisson-分布式集合
3.1、RMap对象
基于Redis的Redisson的分布式映射结构的RMap对象实现了`java.util.concurrent.ConcurrentMap`接口和`java.util.Map`接口。与HashMap不同的是RMap保持了元素的插入顺序。该对象的最大容量受Redis限制,最大元素数量是4 294 967 295个
package com.itheima.redission.service;
import com.itheima.redission.pojo.AnyObject;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @ClassName RMapOpertions.java
* @Description hash表操作
*/
@Slf4j
@Component
public class RMapOpertions {
@Autowired
RedissonClient redissonClient;
public void rMapOper(){
//获得hash表,这里RMapOpertions为主key
RMap<String, AnyObject> userInfo=redissonClient.getMap("userInfo");
AnyObject anyObjectA=AnyObject.builder().id("1").name("嬴政").age(22).address("秦朝").build();
AnyObject anyObjectB=AnyObject.builder().id("2").name("李斯").age(22).address("秦朝").build();
AnyObject anyObjectC=AnyObject.builder().id("3").name("孙悟空").age(22).address("唐").build();
//添加元素,返回的值为之前hash表中的值
userInfo.put(anyObjectA.getId(),anyObjectA);
userInfo.put(anyObjectB.getId(),anyObjectB);
userInfo.put(anyObjectC.getId(),anyObjectC);
//获得userInfo中所有的key
Set<String> keySet=userInfo.readAllKeySet();
log.info("获得userInfo中所有的key:{}",keySet.toString());
//获得userInfo中所有的values
Collection<AnyObject> anyObjects=userInfo.readAllValues();
log.info("获得userInfo中所有的值:{}",anyObjects.toString());
//获得userInfo中所有的元素对象
Set<Map.Entry<String, AnyObject>> entries=userInfo.readAllEntrySet();
log.info("获得userInfo中所有的元素对象:{}",entries.toString());
userInfo.clear();
//快速添加元素,与put的不同是不返回值,且添加速度快
userInfo.fastPut(anyObjectA.getId(),anyObjectA);
userInfo.fastPut(anyObjectB.getId(),anyObjectB);
userInfo.fastPut(anyObjectC.getId(),anyObjectC);
userInfo.clear();
//批量添加
Map<String,AnyObject> map=new HashMap<>();
map.put(anyObjectA.getId(),anyObjectA);
map.put(anyObjectB.getId(),anyObjectB);
map.put(anyObjectC.getId(),anyObjectC);
userInfo.putAll(map);
//根据辅key获得元素,【和map中获得元素一样】
AnyObject anyObjectResult=userInfo.get(anyObjectA.getId());
log.info("根据辅key获得元素对象:{}",anyObjectResult.toString());
//试着添加元素,如果元素key存在则不做任何修改,,如果元素key不存在则做修改,
//返回结果为之前值【如果返回null,表明之前每页存储过元素】
AnyObject anyObjectD=AnyObject.builder()
.id("4").name("如来佛").age(1000000).address("上古").build();
AnyObject anyObject=userInfo.putIfAbsent(anyObjectD.getId(), anyObjectD);
//清除所有元素
userInfo.clear();
}
}
3.2、RMapCache对象
Redisson的分布式的RMapCache象在基于RMap的前提下实现了针对单个元素的淘汰机制。同时仍然保留了元素的插入顺序。由于RMapCache是基于RMap实现的,使它同时继承了java.util.concurrent.ConcurrentMap接口和java.util.Map接口。
package com.itheima.redission.service;
import com.itheima.redission.pojo.AnyObject;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RMap;
import org.redisson.api.RMapCache;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @ClassName RMapCacheOpertions.java
* @Description 带淘汰机制的hash表操作
*/
@Slf4j
@Component
public class RMapCacheOpertions {
@Autowired
RedissonClient redissonClient;
public void rMapCache(){
//获得hash表,这里RMapOpertions为主key
RMapCache<String, AnyObject> userInfo=redissonClient.getMapCache("userInfo");
AnyObject anyObjectA=AnyObject.builder().id("1").name("嬴政").age(22).address("秦朝").build();
AnyObject anyObjectB=AnyObject.builder().id("2").name("李斯").age(22).address("秦朝").build();
AnyObject anyObjectC=AnyObject.builder().id("3").name("孙悟空").age(22).address("唐").build();
//添加元素,返回的值为之前hash表中的值,并且为每个子元素添加过期时间
userInfo.put(anyObjectA.getId(),anyObjectA,20, TimeUnit.SECONDS);
userInfo.put(anyObjectB.getId(),anyObjectB,20, TimeUnit.SECONDS);
userInfo.put(anyObjectC.getId(),anyObjectC,20, TimeUnit.SECONDS);
//获得userInfo中所有的key
Set<String> keySet=userInfo.readAllKeySet();
log.info("获得userInfo中所有的key:{}",keySet.toString());
//获得userInfo中所有的values
Collection<AnyObject> anyObjects=userInfo.readAllValues();
log.info("获得userInfo中所有的值:{}",anyObjects.toString());
//获得userInfo中所有的元素对象
Set<Map.Entry<String, AnyObject>> entries=userInfo.readAllEntrySet();
log.info("获得userInfo中所有的元素对象:{}",entries.toString());
userInfo.clear();
//其他操作与RMap类似,这里就不再操作
}
}
==注意:==目前的Redis自身并不支持散列(Hash)当中的元素淘汰,因此所有过期元素都是通过org.redisson.EvictionScheduler实例来实现定期清理的。为了保证资源的有效利用,每次运行最多清理300个过期元素。任务的启动时间将根据上次实际清理数量自动调整,间隔时间趋于1秒到1小时之间。比如该次清理时删除了300条元素,那么下次执行清理的时间将在1秒以后(最小间隔时间)。一旦该次清理数量少于上次清理数量,时间间隔将增加1.5倍
4、redisson-分布式锁
4.1、原理分析
- 加锁机制
线程去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。
线程去获取锁,获取失败: 一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis - WatchDog自动延期机制
第一种情况:在一个分布式环境下,假如一个线程获得锁后,突然服务器宕机了,那么这个时候在一定时间后这个锁会自动释放,你也可以设置锁的有效时间(不设置默认30秒),这样的目的主要是防止死锁的发生
第二种情况:线程A业务还没有执行完,时间就过了,线程A 还想持有锁的话,就会启动一个watch dog后台线程,不断的延长锁key的生存时间 - lua脚本
主要是如果你的业务逻辑复杂的话,通过封装在lua脚本中发送给redis,而且redis是单线程的,这样就保证这段复杂业务逻辑执行的原子性
4.2、基本使用
很明显RLock是继承Lock锁,所以他有Lock锁的所有特性,比如lock、unlock、trylock等特性,同时它还有很多新特性:强制锁释放,带有效期的锁,。
public interface RLock {
//----------------------Lock接口方法-----------------------
/**
* 加锁 锁的有效期默认30秒
*/
void lock();
/**
* 加锁 可以手动设置锁的有效时间
*
* @param leaseTime 锁有效时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
void lock(long leaseTime, TimeUnit unit);
/**
* tryLock()方法是有返回值的,用来尝试获取锁,
* 如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false .
*/
boolean tryLock();
/**
* tryLock(long waitTime, TimeUnit unit)方法和tryLock()方法是类似的,
* 只不过区别在于这个方法在拿不到锁时会等待一定的时间,
* 在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
*
* @param time 等待时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException;
/**
* 比上面多一个参数,多添加一个锁的有效时间
*
* @param waitTime 等待时间
* @param leaseTime 锁有效时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
/**
* 解锁
*/
void unlock();
}
lock():此方法为加锁,但是锁的有效期采用默认30秒,如果主线程未释放,且当前锁未调用unlock方法,则进入到watchDog机制,如果主线程未释放,且当前锁调用unlock方法,则直接释放锁
public void lock() throws InterruptedException{
log.info("线程:{},进入方法",Thread.currentThread().getName());
RLock rLock=redissonClient.getLock("lock");
//加锁:锁的有效期默认30秒
rLock.lock();
long timeToLive=rLock.remainTimeToLive();
log.info("线程:{},获得锁,锁存活时间:{}S",Thread.currentThread().getName(),timeToLive/1000);
//休眠一下
Thread.sleep(2000);
//如果主线程未释放,且当前锁未调用unlock方法,则进入到watchDog机制
//如果主线程未释放,且当前锁调用unlock方法,则直接释放锁
rLock.unlock();
log.info("线程:{},释放锁",Thread.currentThread().getName());
}
lock(long leaseTime, TimeUnit unit):可以手动设置锁的有效时间,如果主线程未释放,且当前锁未调用unlock方法,则锁到期后会自动释放,如果主线程未释放,且当前锁调用unlock方法,则直接释放锁
public void lockLaseTime() throws InterruptedException{
log.info("线程:{},进入方法",Thread.currentThread().getName());
RLock rLock=redissonClient.getLock("lockLaseTime");
//加锁 上面是默认30秒,
//这里可以手动设置锁的有效时间,锁到期后会自动释放的
rLock.lock(10,TimeUnit.SECONDS);
long timeToLive=rLock.remainTimeToLive();
log.info("线程:{},获得锁,锁存活时间:{}S",Thread.currentThread().getName(),timeToLive/1000);
//休眠一下
Thread.sleep(2000);
//如果主线程未释放,且当前锁未调用unlock方法,则锁到期后会自动释放
//如果主线程未释放,且当前锁调用unlock方法,则直接释放锁
rLock.unlock();
log.info("线程:{},释放锁",Thread.currentThread().getName());
}
tryLock():用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,如果主线程未释放,且当前锁未调用unlock方法,则进入到watchDog机制,如果主线程未释放,且当前锁调用unlock方法,则直接释放锁
public void tryLock() throws InterruptedException {
log.info("线程:{},进入方法",Thread.currentThread().getName());
RLock rLock=redissonClient.getLock("tryLock");
//tryLock()方法是有返回值的,它表示用来尝试获取锁,
//如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false .
boolean flag=rLock.tryLock();
if (flag){
long timeToLive=rLock.remainTimeToLive();
log.info("线程:{},获得锁,锁存活时间:{}S,加锁状态:{}",Thread.currentThread().getName(),timeToLive/1000,flag);
//休眠一下
Thread.sleep(2000);
//如果主线程未释放,且当前锁未调用unlock方法,则进入到watchDog机制
//如果主线程未释放,且当前锁调用unlock方法,则直接释放锁
rLock.unlock();
log.info("线程:{},释放锁",Thread.currentThread().getName());
}else {
log.info("线程:{},获得锁失败",Thread.currentThread().getName());
}
}
tryLock(long time, TimeUnit unit):tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间, 在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true,如果主线程未释放,且当前锁未调用unlock方法,则进入到watchDog机制如果主线程未释放,且当前锁调用unlock方法,则直接释放锁
public void tryLockWaitTime() throws InterruptedException {
log.info("线程:{},进入方法",Thread.currentThread().getName());
RLock rLock=redissonClient.getLock("tryLockWaitTime");
//tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,
//只不过区别在于这个方法在拿不到锁时会等待一定的时间,
//在时间期限之内如果还拿不到锁,就返回false如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
boolean flag=rLock.tryLock(6, TimeUnit.SECONDS);
if (flag){
long timeToLive=rLock.remainTimeToLive();
log.info("线程:{},获得锁,锁存活时间:{}S,加锁状态:{}",Thread.currentThread().getName(),timeToLive/1000,flag);
//休眠一下
Thread.sleep(10000);
//如果主线程未释放,且当前锁未调用unlock方法,则进入到watchDog机制
//如果主线程未释放,且当前锁调用unlock方法,则直接释放锁
rLock.unlock();
log.info("线程:{},释放锁",Thread.currentThread().getName());
}else {
log.info("线程:{},获得锁失败",Thread.currentThread().getName());
}
}
tryLock(long waitTime, long leaseTime, TimeUnit unit):比上面多一个参数,多添加一个锁的有效时间
public void tryLockleasTime() throws InterruptedException {
log.info("线程:{},进入方法",Thread.currentThread().getName());
RLock rLock = redissonClient.getLock("tryLockleasTime");
//比上面多一个参数,多添加一个锁的有效时间
boolean flag = rLock.tryLock(11,10, TimeUnit.SECONDS);
if (flag){
long timeToLive = rLock.remainTimeToLive();
log.info("线程:{},获得锁,锁存活时间:{}S,加锁状态:{}",Thread.currentThread().getName(),timeToLive/1000,flag);
//休眠一下
Thread.sleep(6000);
//如果主线程未释放,且当前锁未调用unlock方法,则锁到期后会自动释放的
//如果主线程未释放,且当前锁调用unlock方法,则直接释放锁
rLock.unlock();
log.info("线程:{},释放锁",Thread.currentThread().getName());
}else {
log.info("线程:{},获得锁失败",Thread.currentThread().getName());
}
}
第二章 点餐平台-开桌、主体信息、订单详情
下面我们进入的餐掌柜的核心业务—点餐,点餐我们采用的是小程序开发,用户通过扫码进入系统,然后完成点餐、下单等操作
1、功能区拆解
下面我们首先看下整个的业务流程:
具体流程如下:
- 用户通过二维码扫码,进入门店点餐系统
- 扫码之后,查询店铺主体信息,查询桌台是否开台
- 已开台:查询当前桌台订单信息【包括可核算订单项和购物车订单项】
- 未开台:选择就餐人数,创建订单
- 查询桌台订单信息,返回主体信息
2、数据库结构
上图为整个桌与相关管理表的关系,各位需要注意,订单项的信息存放在2个位置:
- Mysql:用户已经下单,可以作为核算的订单项
- Redis:用户加入购物车,但是未下单的作为临时存储的订单项
- 在用户执行下单时,我们会吧Redis的购物车订单项会合并到可核算订单项中
3、功能开发
model-shop-applet模块为H5程序微服务的生产,其核心uml类图如下:
3.1、桌台是否开台
桌台是否开台的前提条件:
- 当前桌台处于空闲状态且无【待付款、支付中】订单,认为桌台未开台,可以使用
- 当前桌台存在处于【待付款、支付中】的订单,认为当前桌台已开台
- AppletController:
传入tableId直接调用appletFace.isOpen(tableId)返回已开台:true,未开台:false
@DubboReference(version = "${dubbo.application.version}",check = false)
AppletFace appletFace;
@GetMapping("is-open/{tableId}")
@ApiOperation(value = "查询是否开桌",notes = "是否开桌,已开台:进入继续点餐流程,未开台:进入开台流程")
@ApiImplicitParam(paramType = "path",name = "tableId",value = "桌台",dataType = "Long")
public ResponseWrap<Boolean> isOpen(@PathVariable("tableId") Long tableId) {
Boolean isOpen = appletFace.isOpen(tableId);
return ResponseWrapBuild.build(TableEnum.SUCCEED,isOpen);
}
- AppletFace
/***
* @description 是否开桌,已开台:进入继续点餐流程,未开台:进入开台流程
* @param tableId 桌台Id
* @return Boolean
*/
Boolean isOpen(Long tableId)throws ProjectException;
- AppletFaceImpl
1、查询桌台信息,是否为空闲
2、是否已经有【待支付、支付中】订单存在
/**
* @ClassName AppletFaceImpl.java
* @Description 小程序H5实现
*/
@Slf4j
@DubboService(version = "${dubbo.application.version}", timeout = 5000,
methods = {
@Method(name = "isOpen", retries = 2)
}
)
public class AppletFaceImpl implements AppletFace {
@Autowired
IOrderService orderService;
@Autowired
ITableService tableService;
@Override
public Boolean isOpen(Long tableId) throws ProjectException{
try {
//1、查询桌台信息,是否为使用中
Table table = tableService.getById(tableId);
Boolean flagTableStatus = table.getTableStatus().equals(SuperConstant.USE);
//2、是否已经有【待支付、支付中】订单存在
OrderVo orderVoResult = orderService.findOrderByTableId(tableId);
Boolean flagOrderVo = !EmptyUtil.isNullOrEmpty(orderVoResult);
if (flagTableStatus||flagOrderVo){
return true;
}
return false;
}catch (Exception e){
log.error("查询桌台信息异常:{}", ExceptionsUtil.getStackTraceAsString(e));
throw new ProjectException(TableEnum.SELECT_TABLE_FAIL);
}
}
}
- OrderServiceImpl
1、查询当前处于待支付,付款中的有效订单
@Override
public OrderVo findOrderByTableId(Long tableId) {
QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(Order::getTableId,tableId);
queryWrapper.lambda().eq(Order::getEnableFlag,SuperConstant.YES);
queryWrapper.lambda().and(wrapper->wrapper
.eq(Order::getOrderState,SuperConstant.DFK)
.or()
.eq(Order::getOrderState,SuperConstant.FKZ));
Order order = getOne(queryWrapper);
return BeanConv.toBean(order,OrderVo.class);
}
3.2、相关主体信息
无论用户是首次开台或者是进入已经开台的桌台,都需要展现品牌、门店、桌台、菜品分类、菜品、等相关的信息,这里包装了一个服务接口一次性把所有的数据都拿到。
- AppletController
AppletInfoVo对象包所有相关信息,注意:品牌图片信息、菜品图片口味信息需要调用通用服务获得
@DubboReference(version = "${dubbo.application.version}",check = false)
AppletFace appletFace;
@GetMapping("table-appletInfo/{tableId}")
@ApiOperation(value = "查询桌台相关主体信息",notes = "查询桌台相关主体信息:品牌、门店、菜品、口味、分类等")
@ApiImplicitParam(paramType = "path",name = "tableId",value = "桌台Id",dataType = "Long")
public ResponseWrap<AppletInfoVo> findAppletInfoVoByTableId(@PathVariable("tableId") Long tableId){
//AppletInfoVo对象包所有相关信息,注意:品牌图片信息、菜品图片口味信息需要调用通用服务获得
AppletInfoVo appletInfoVo = appletFace.findAppletInfoVoByTableId(tableId);
return ResponseWrapBuild.build(TableEnum.SUCCEED,appletInfoVo);
}
- AppletFace
/***
* @description 查询桌台相关主体信息
* @param tableId 桌台Id
* @return AppletInfoVo
*/
AppletInfoVo findAppletInfoVoByTableId(Long tableId);
- AppletFaceImpl
1、查询桌台
2、查询门店
3、查询品牌
3.1、处理品牌图片
4、查询分类
5、查询菜品
6、查询菜品口味、图片信息
6.1、口味与数字字典中间表信息
6.2、构建数字字典dataKeys
6.3、RPC查询数字字典口味信息
6.4、RPC查询附件信息
7、构建返回对象
@Autowired
ITableService tableService;
@Autowired
ICategoryService categoryService;
@Autowired
IDishService dishService;
@Autowired
IDishFlavorService dishFlavorService;
@Autowired
IBrandService brandService;
@Autowired
IStoreService storeService;
@Override
public AppletInfoVo findAppletInfoVoByTableId(Long tableId)throws ProjectException {
try {
//1、查询桌台信息
Table table = tableService.getById(tableId);
TableVo tableVo = BeanConv.toBean(table, TableVo.class);
//2、查询门店
Store store = storeService.getById(table.getStoreId());
StoreVo storeVo = BeanConv.toBean(store, StoreVo.class);
//3、查询品牌
Brand brand = brandService.getById(store.getBrandId());
BrandVo brandVo = BeanConv.toBean(brand, BrandVo.class);
//3.1、处理品牌图片
List<AffixVo> affixVoListBrand = affixFace.findAffixVoByBusinessId(brandVo.getId());
brandVo.setAffixVo(affixVoListBrand.get(0));
//4、查询分类
List<Category> categorys = categoryService.findCategoryVoByStoreId(table.getStoreId());
List<CategoryVo> categoryVoList = BeanConv.toBeanList(categorys, CategoryVo.class);
//5、查询菜品
List<Dish> dishs = dishService.findDishVoByStoreId(table.getStoreId());
List<DishVo> dishVos = BeanConv.toBeanList(dishs, DishVo.class);
//6、查询菜品口味、图片信息
dishVos.forEach(dishVo->{
//6.1、口味与数字字典中间表信息
List<DishFlavor> dishFlavors = dishFlavorService.findDishFlavorByDishId(dishVo.getId());
List<DishFlavorVo> dishFlavorVos = BeanConv.toBeanList(dishFlavors, DishFlavorVo.class);
dishVo.setDishFlavorVos(dishFlavorVos);
//6.2、构建数字字典dataKeys
List<String> dataKeys = dishFlavorVos.stream()
.map(DishFlavorVo::getDataKey).collect(Collectors.toList());
//6.3、RPC查询数字字典口味信息
List<DataDictVo> valueByDataKeys = dataDictFace.findValueByDataKeys(dataKeys);
dishVo.setDataDictVos(valueByDataKeys);
//6.4、RPC查询附件信息
List<AffixVo> affixVoListDish = affixFace.findAffixVoByBusinessId(dishVo.getId());
dishVo.setAffixVo(affixVoListDish.get(0));
});
//7、构建返回对象
AppletInfoVo appletInfoVo = AppletInfoVo.builder()
.tableVo(tableVo)
.storeVo(storeVo)
.brandVo(brandVo)
.categoryVos(categoryVoList)
.dishVos(dishVos)
.build();
return appletInfoVo;
} catch (Exception e) {
log.error("查询桌台相关主体信息异常:{}", ExceptionsUtil.getStackTraceAsString(e));
throw new ProjectException(TableEnum.SELECT_TABLE_FAIL);
}
}
- IDishService
/***
* @description 查询店铺下所有起售且有效菜品
* @param storeId
* @return List<Dish>
*/
List<Dish> findDishVoByStoreId(Long storeId);
- DishServiceImpl
@Override
public List<Dish> findDishVoByStoreId(Long storeId) {
LambdaQueryWrapper<Dish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(Dish::getStoreId,storeId)
.eq(Dish::getEnableFlag,SuperConstant.YES)
.eq(Dish::getDishStatus,SuperConstant.YES);
return list(lambdaQueryWrapper);
}
3.3、用户开桌操作
调用【3.1桌台是否开台】接口后,如果桌台未开台,可以调用用户开桌操作接口,指定就餐人数进行开台操作,开台操作主要完成:
1、为桌台创建【待支付】订单
2、修改桌台状态【空闲——>开桌】
- AppletController
传入桌台ID和就餐人数,进行开台操作
@PostMapping("open-table/{tableId}/{personNumbers}")
@ApiOperation(value = "开桌操作",notes = "未开桌:选择人数创建订单")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "path",name = "tableId",value = "桌台",dataType = "Long"),
@ApiImplicitParam(paramType = "path",name = "personNumbers",value = "就餐人数",dataType = "Integer"),
})
public ResponseWrap<OrderVo> openTable(
@PathVariable("tableId") Long tableId,
@PathVariable("personNumbers") Integer personNumbers) {
OrderVo orderVoResult = appletFace.openTable(tableId,personNumbers);
return ResponseWrapBuild.build(BrandEnum.SUCCEED,orderVoResult);
}
- AppletFace
/***
* @description 未开桌:选择人数创建订单
* @param tableId 桌台Id
* @param personNumbers 就餐人数
* @return Boolean
*/
OrderVo openTable(Long tableId,Integer personNumbers)throws ProjectException;
- AppletFaceImpl
幂等性:是指无论调用多少次都不会有不同结果的 HTTP 方法。不管你调用一次,还是调用一百次,一千次,结果都是相同的。1、开台状态定义
2、锁定桌台,防止并发重复创建订单
3、幂等性:再次查询桌台订单情况
4、未开台,为桌台创建当订单
4.1、查询桌台信息
4.2、构建订单
5、修改桌台状态为使用中
6、订单处理:处理可核算订单项和购物车订单项,可调用桌台订单显示接口
@Override
@Transactional
public OrderVo openTable(Long tableId,Integer personNumbers) throws ProjectException {
//1、开台状态定义
boolean flag = true;
//2、锁定桌台,防止并发重复创建订单
String key = AppletCacheConstant.OPEN_TABLE_LOCK+tableId;
RLock lock = redissonClient.getLock(key);
try {
if(lock.tryLock(AppletCacheConstant.REDIS_WAIT_TIME,
AppletCacheConstant.REDIS_LEASETIME,
TimeUnit.SECONDS)){
//3、幂等性:再次查询桌台订单情况
OrderVo orderVoResult = orderService.findOrderByTableId(tableId);
//4、未开台,为桌台创建当订单
if (EmptyUtil.isNullOrEmpty(orderVoResult)){
//4.1、查询桌台信息
Table table = tableService.getById(tableId);
//4.2、构建订单
Order order = Order.builder()
.tableId(tableId)
.tableName(table.getTableName())
.storeId(table.getStoreId())
.areaId(table.getAreaId())
.enterpriseId(table.getEnterpriseId())
.orderNo((Long) identifierGenerator.nextId(tableId))
.orderState(TradingConstant.DFK)
.isRefund(SuperConstant.NO)
.refund(new BigDecimal(0))
.discount(new BigDecimal(10))
.personNumbers(personNumbers)
.reduce(new BigDecimal(0))
.useScore(0)
.acquireScore(0l)
.build();
orderService.save(order);
//5、修改桌台状态为使用中
TableVo tableVo = TableVo.builder()
.id(tableId)
.tableStatus(SuperConstant.USE).build();
tableService.updateTable(tableVo);
}
}
//6、订单处理:处理可核算订单项和购物车订单项,可调用桌台订单显示接口
return showOrderVoforTable(tableId);
}catch (Exception e){
log.error("开桌操作异常:{}", ExceptionsUtil.getStackTraceAsString(e));
throw new ProjectException(TableEnum.OPEN_TABLE_FAIL);
}finally {
lock.unlock();
}
}
3.4、菜品信息详情
用户调用【3.2相关主体信息】后,可以点击菜品。查询菜品详情,并且选择自己的菜品口味,效果图如下:
- AppletController
1、查询菜品信息,注意:菜品图片口味信息需要调用通用服务获得
2、处理菜品口味
3、处理菜品图片
4、封装返回结果
@PostMapping("dish-details/{dishId}")
@ApiOperation(value = "查询菜品详情",notes = "显示菜品详情,包括口味")
@ApiImplicitParam(paramType = "path",name = "dishId",value = "菜品Id",dataType = "菜品Id")
public ResponseWrap<DishVo> findDishVoById(@PathVariable("dishId") Long dishId) {
//查询菜品信息,注意:菜品图片口味信息需要调用通用服务获得
DishVo dishVo = appletFace.findDishVoById(dishId);
//封装返回结果
return ResponseWrapBuild.build(BrandEnum.SUCCEED,dishVo);
}
- AppletFace
/***
* @description 查询菜品详情
* @return
* @return: com.itheima.restkeeper.req.DishVo
*/
DishVo findDishVoById(Long dishId) throws ProjectException;
- AppletFaceImpl
1、查询菜品信息,注意:菜品图片口味信息需要调用通用服务获得
2、查询菜品口味
3、处理菜品口味【数字字典】
4、处理菜品图片
@Override
public DishVo findDishVoById(Long dishId)throws ProjectException {
try {
//1、查询菜品,注意:菜品图片口味信息需要调用通用服务获得
Dish dish = dishService.getById(dishId);
//2、查询菜品口味
List<DishFlavor> dishFlavors = dishFlavorService.findDishFlavorByDishId(dishId);
DishVo dishVo = BeanConv.toBean(dish, DishVo.class);
//3、处理菜品口味【数字字典】
List<DishFlavorVo> dishFlavorVos = BeanConv.toBeanList(dishFlavors,DishFlavorVo.class);
List<String> DataKeys = dishFlavorVos.stream()
.map(DishFlavorVo::getDataKey).collect(Collectors.toList());
List<DataDictVo> valueByDataKeys = dataDictFace.findValueByDataKeys(DataKeys);
dishVo.setDataDictVos(valueByDataKeys);
//4、处理菜品图片
List<AffixVo> affixVoListDish = affixFace.findAffixVoByBusinessId(dishVo.getId());
dishVo.setAffixVo(affixVoListDish.get(0));
return dishVo ;
}catch (Exception e){
log.error("查询菜品详情异常:{}", ExceptionsUtil.getStackTraceAsString(e));
throw new ProjectException(DishEnum.SELECT_DISH_FAIL);
}
}
3.5、桌台订单信息
如果已经开桌,进入H5订单首页后,我们需要 拿桌台的订单项信息,订单项存储在2个存储中:
1、可核算订单项【已下单可结算】:MySQL
2、购物车订单项【已加入购物车未下单】:Redis
功能效果如图所示:
- AppletController
已开桌:查询当前桌台订单信息【包括可核算订单项和购物车订单项】
@PostMapping("show-ordervo-table/{tableId}")
@ApiOperation(value = "查询桌台订单信息",notes = "已开桌:查询当前桌台订单信息【包括可核算订单项和购物车订单项】")
@ApiImplicitParam(paramType = "path",name = "tableId",value = "桌台",dataType = "Long")
public ResponseWrap<OrderVo> showOrderVoforTable(@PathVariable("tableId") Long tableId) {
OrderVo orderVoResult = appletFace.showOrderVoforTable(tableId);
return ResponseWrapBuild.build(BrandEnum.SUCCEED,orderVoResult);
}
- AppletFace
/***
* @description 已开桌:查询当前桌台订单信息【包括可核算订单项和购物车订单项】
* @param tableId 桌台ID
* @return
*/
OrderVo showOrderVoforTable(Long tableId) throws ProjectException;
/***
* @description 处理当前订单中订单项
* 从DB中查询当前订单可核算订单项
* 从redis查询当前订单购物车订单项
* @param orderVo 订单信息
* @return
*/
OrderVo handlerOrderVo(OrderVo orderVo) throws ProjectException;
/***
* @description 订单项计算
* @param orderItemVos 需要计算的订单项
* @return
* @return: java.math.BigDecimal
*/
BigDecimal reducePriceHandler(List<OrderItemVo> orderItemVos ) throws ProjectException;
- AppletFaceImpl
1、查询MySQL:可核算订单项
2、可核算订单项总金额
2.1、处理空可核算订单项
2.2、计算可核算订单项总金额
3、查询redis:购物车订单项
3.1、计算购物车订单项总金额
4、构建订单信息
@Override
public OrderVo showOrderVoforTable(Long tableId) throws ProjectException {
try {
//查询订单信息
OrderVo orderVoResult = orderService.findOrderByTableId(tableId);
//处理订单:可核算订单项目、购物车订单项目
return handlerOrderVo(orderVoResult);
}catch (Exception e){
log.error("查询桌台订单信息异常:{}", ExceptionsUtil.getStackTraceAsString(e));
throw new ProjectException(OrderEnum.SELECT_TABLE_ORDER_FAIL);
}
}
/***
* @description 处理当前订单中订单项
* 从DB中查询当前订单可核算订单项
* 从redis查询当前订单购物车订单项
* @param orderVo 订单信息
* @return
*/
@Override
public OrderVo handlerOrderVo(OrderVo orderVo)throws ProjectException {
if (!EmptyUtil.isNullOrEmpty(orderVo)) {
//1、查询MySQL:可核算订单项
List<OrderItem> orderItemList = orderItemService.findOrderItemByOrderNo(orderVo.getOrderNo());
List<OrderItemVo> orderItemVoStatisticsList = BeanConv.toBeanList(orderItemList, OrderItemVo.class);
//2、可核算订单项总金额
BigDecimal reducePriceStatistics = new BigDecimal("0");
//2.1、处理空可核算订单项
if (EmptyUtil.isNullOrEmpty(orderItemVoStatisticsList)) {
orderItemVoStatisticsList = new ArrayList<>();
}else {
orderItemVoStatisticsList.forEach(n->{
n.setAffixVo(affixFace.findAffixVoByBusinessId(n.getDishId()).get(0));
});
//2.2、计算可核算订单项总金额
reducePriceStatistics = reducePriceHandler(orderItemVoStatisticsList);
}
//3、查询redis:购物车订单项
String key = AppletCacheConstant.ORDERITEMVO_STATISTICS + orderVo.getOrderNo();
RMapCache<Long, OrderItemVo> orderItemVoRMap = redissonClient.getMapCache(key);
List<OrderItemVo> orderItemVoTemporaryList = (List<OrderItemVo>) orderItemVoRMap.readAllValues();
//3.1、计算购物车订单项总金额
BigDecimal reducePriceTemporary=reducePriceHandler(orderItemVoTemporaryList);
//4、构建订单信息
orderVo.setOrderItemVoStatisticsList(orderItemVoStatisticsList);
orderVo.setReducePriceStatistics(reducePriceStatistics);
orderVo.setOrderItemVoTemporaryList(orderItemVoTemporaryList);
orderVo.setReducePriceTemporary(reducePriceTemporary);
}
return orderVo;
}
@Override
public BigDecimal reducePriceHandler(List<OrderItemVo> orderItemVos )throws ProjectException{
return orderItemVos.stream().map(orderItemVo -> {
BigDecimal price = orderItemVo.getPrice();
BigDecimal reducePrice = orderItemVo.getReducePrice();
Long dishNum = orderItemVo.getDishNum();
//如果有优惠价格以优惠价格计算
if (EmptyUtil.isNullOrEmpty(reducePrice)) {
return price.multiply(new BigDecimal(dishNum));
} else {
return reducePrice.multiply(new BigDecimal(dishNum));
}
}).reduce(BigDecimal.ZERO, BigDecimal::add);
}
- OrderItemServiceImpl
@Override
public List<OrderItem> findOrderItemByOrderNo(Long orderNo) {
LambdaQueryWrapper<OrderItem> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(OrderItem::getProductOrderNo,orderNo);
return list(lambdaQueryWrapper);
}
课堂讨论
1、redis主从模式、哨兵模式、集群模式分别是什么,有什么特点?
[https://www.cnblogs.com/wa1l-E/p/15348755.html](https://www.cnblogs.com/wa1l-E/p/15348755.html)
2、redis的持久化方式aof,rdb是什么?
3、redis的存储类型有那些?
4、redis的过期策略有那些?
5、redisson如何配置Single和Cluster模式?
6、redisson分布式锁原理及常用使用API有那些?
[https://blog.csdn.net/m0_53474063/article/details/113381122](https://blog.csdn.net/m0_53474063/article/details/113381122)
7、双写一致性如何解决?
[https://juejin.cn/post/6964531365643550751](https://juejin.cn/post/6964531365643550751)
8、redis数据分布算法有哪些?
9、简述开桌流程,开桌中我们为什么要加分布式锁?其注意事项有那些?
10、桌台订单信息中购物车订单项、可核算车订单项意义?
11、如何让多人同时点餐,并且能实时看到对方点的菜品?
12、什么是幂等性,实现幂等性的方案有哪些?
课后任务
1、完成对redisson客户端的debug过程
2、完成当天课堂讨论,同步到git【☆☆☆☆☆】
3、完成2-5道sql练习【☆☆☆☆】
4、完成点餐平台-开桌、主体信息、订单详情开发,同步到git【☆☆☆☆☆】
5、梳理项目二-点餐平台-开桌、主体信息、订单详情业务,同步到git【☆☆☆】