学习目标
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:#如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,#那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。#默认值:10000idleConnectionTimeout: 10000pingTimeout: 1000#同任何节点建立连接时的等待超时。时间单位是毫秒。#默认值:10000connectTimeout: 10000#等待节点回复命令的时间。该时间从命令发送成功时开始计时。#默认值:3000timeout: 3000#如果尝试达到 retryAttempts(命令失败重试次数)#仍然不能将命令发送至某个指定的节点时,将抛出错误。如果尝试在此限制之内发送成功,#则开始启用 timeout(命令等待超时) 计时#默认值:3retryAttempts: 3#在某个节点执行相同或不同命令时,连续失败failedAttempts(执行失败最大次数)时,#该节点将被从可用节点列表里清除,直到 reconnectionTimeout(重新连接时间间隔) 超时以后再次尝试。#默认值:1500retryInterval: 1500#重新连接时间间隔reconnectionTimeout: 3000#执行失败最大次数failedAttempts: 3#密码password: null#每个连接的最大订阅数量。#默认值:5subscriptionsPerConnection: 5#在Redis节点里显示的客户端名称。clientName: null#在Redis节点address: "redis://192.168.112.77:6379"#从节点发布和订阅连接的最小空闲连接数#默认值:1subscriptionConnectionMinimumIdleSize: 1#用于发布和订阅连接的连接池最大容量。连接池的连接数量自动弹性伸缩。#默认值:50subscriptionConnectionPoolSize: 50#节点最小空闲连接数#默认值:32connectionMinimumIdleSize: 32#节点连接池大小#默认值:64connectionPoolSize: 64#这个线程池数量被所有RTopic对象监听器,RRemoteService调用者和RExecutorService任务共同共享。#默认值: 当前处理核数量 * 2threads: 8#这个线程池数量是在一个Redisson实例内,被其创建的所有分布式数据类型和服务,#以及底层客户端所一同共享的线程池里保存的线程数量。#默认值: 当前处理核数量 * 2nettyThreads: 8#Redisson的对象编码类是用于将对象进行序列化和反序列化,以实现对该对象在Redis里的读取和存储。#默认值: org.redisson.codec.JsonJacksonCodeccodec: !<org.redisson.codec.JsonJacksonCodec> {}#传输模式#默认值:TransportMode.NIOtransportMode: "NIO"
1.2、Cluster节点配置
配置集群模式可以通过指定一个YAML格式的文件来实现。以下是YAML格式的配置文件样本。文件中的字段名称必须与clusterServersConfig和config对象里的字段名称相符。
---clusterServersConfig:#如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,#那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。#默认值:10000idleConnectionTimeout: 10000#同任何节点建立连接时的等待超时。时间单位是毫秒。#默认值:10000connectTimeout: 10000#等待节点回复命令的时间。该时间从命令发送成功时开始计时。#默认值:3000timeout: 3000#如果尝试达到 retryAttempts(命令失败重试次数)#仍然不能将命令发送至某个指定的节点时,将抛出错误。如果尝试在此限制之内发送成功,#则开始启用 timeout(命令等待超时) 计时。#默认值:3retryAttempts: 3#在某个节点执行相同或不同命令时,连续失败failedAttempts(执行失败最大次数)时,#该节点将被从可用节点列表里清除,直到 reconnectionTimeout(重新连接时间间隔) 超时以后再次尝试。#默认值:1500retryInterval: 1500#密码password: pass#每个连接的最大订阅数量。#默认值:5subscriptionsPerConnection: 5clientName: 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: 1slaveSubscriptionConnectionPoolSize: 50slaveConnectionMinimumIdleSize: 32slaveConnectionPoolSize: 64masterConnectionMinimumIdleSize: 32masterConnectionPoolSize: 64readMode: "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: 1000threads: 8nettyThreads: 8codec: !<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@Componentpublic class KeyOpertions {@AutowiredRedissonClient redissonClient;public void foundedKeys(){RBucket<Object> bucket = redissonClient.getBucket("security:aa");bucket.set("张三");//获得所有keysRKeys keys = redissonClient.getKeys();Iterable<String> keysAll = keys.getKeys();for (String key : keysAll) {log.info("获得key:"+key);}//获得所有security开头的keyIterable<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("张成成女朋友");//获得所有keysRKeys 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@Componentpublic class BucketOpertions {@AutowiredRedissonClient redissonClient;/**** @description Bucket通用对象通*/public void bucketOper(){//获得BucketRBucket<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);//移除BucketOperTestboolean deleteFalg = anyObjectRBucket.delete();log.info("BucketOperTest删除:{}",deleteFalg);}/**** @description Buckets批量通用对象通*/public void bucketsOper(){//获得BucketRBucket<AnyObject> bucketA = redissonClient.getBucket("BucketOpertionsTestA");//放入一个元素AnyObject anyObject = AnyObject.builder().name("张三").age(19).address("中国上海").build();//为BucketOperTest添加元素到redis中bucketA.set(anyObject);//获得BucketRBucket<AnyObject> bucketB = redissonClient.getBucket("BucketOpertionsTestB");bucketB.set(anyObject);//获得BucketsRBuckets 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(){//获得BucketRBucket<AnyObject> bucketA=redissonClient.getBucket("BucketOperTestA");//放入一个元素AnyObject anyObject=AnyObject.builder().name("张三").age(19).address("中国上海").build();//为BucketOperTest添加元素到redis中bucketA.set(anyObject);//获得BucketRBucket<AnyObject> bucketB=redissonClient.getBucket("BucketOperTestB");bucketB.set(anyObject);//获得BucketsRBuckets 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@Componentpublic class AtomicLongOpertions {@AutowiredRedissonClient 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);//先获得元素,再递增1flag=atomicLongOper.getAndIncrement();log.info("先获得元素,再递增1",flag);//获得当前元素flag=atomicLongOper.get();log.info("获得当前元素:{}",flag);//先递减1,然后返回元素flag=atomicLongOper.decrementAndGet();log.info("先递减1,然后返回元素:{}",flag);//先获得元素,再递增1flag=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@Componentpublic class AtomicDoubleOpertions {@AutowiredRedissonClient 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);//先获得元素,再递增1flag=atomicDoubleOper.getAndIncrement();log.info("先获得元素,再递增1",flag);//获得当前元素flag=atomicDoubleOper.get();log.info("获得当前元素:{}",flag);//先递减1,然后返回元素flag=atomicDoubleOper.decrementAndGet();log.info("先递减1,然后返回元素:{}",flag);//先获得元素,再递增1flag=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@Componentpublic class RMapOpertions {@AutowiredRedissonClient redissonClient;public void rMapOper(){//获得hash表,这里RMapOpertions为主keyRMap<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中所有的keySet<String> keySet=userInfo.readAllKeySet();log.info("获得userInfo中所有的key:{}",keySet.toString());//获得userInfo中所有的valuesCollection<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@Componentpublic class RMapCacheOpertions {@AutowiredRedissonClient redissonClient;public void rMapCache(){//获得hash表,这里RMapOpertions为主keyRMapCache<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中所有的keySet<String> keySet=userInfo.readAllKeySet();log.info("获得userInfo中所有的key:{}",keySet.toString());//获得userInfo中所有的valuesCollection<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 {@AutowiredIOrderService orderService;@AutowiredITableService tableService;@Overridepublic 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、查询当前处于待支付,付款中的有效订单
@Overridepublic 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、构建数字字典dataKeys6.3、RPC查询数字字典口味信息6.4、RPC查询附件信息
7、构建返回对象
@AutowiredITableService tableService;@AutowiredICategoryService categoryService;@AutowiredIDishService dishService;@AutowiredIDishFlavorService dishFlavorService;@AutowiredIBrandService brandService;@AutowiredIStoreService storeService;@Overridepublic 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、构建数字字典dataKeysList<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
@Overridepublic 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@Transactionalpublic 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、处理菜品图片
@Overridepublic 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、构建订单信息
@Overridepublic 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*/@Overridepublic 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;}@Overridepublic 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
@Overridepublic 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【☆☆☆】
