学习目标

1、掌握redisson分布式对象API
2、掌握redisson分布式集合API
3、掌握redisson分布式锁API
4、完成桌台是否开桌功能
5、完成主体信息查询功能
6、复述出用户开桌操作流程、理解开桌加锁意义
7、完成菜品信息详情功能
8、理解桌台订单信息中购物车订单项、可核算订单项意义

第一章 redisson框架

  1. Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务

image.png

  1. Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

1、redisson快速入门

这里我们使用spring-boot集成redission,首先需要在pom.xml文件中添加依赖

  1. <properties>
  2. <redisson-spring-boot>3.11.2</redisson-spring-boot>
  3. </properties>
  4. <!--redis缓存客户端-->
  5. <dependency>
  6. <groupId>org.redisson</groupId>
  7. <artifactId>redisson-spring-boot-starter</artifactId>
  8. <version>${redisson-spring-boot}</version>
  9. </dependency>

在项目的resources目录中application.yml添加

  1. spring:
  2. redis:
  3. redisson:
  4. #配置文件目录
  5. config: classpath:singleServerConfig.yaml
  6. #config: classpath:clusterServersConfig.yaml

1.1、Single节点配置

  1. 配置单节点模式可以通过在resources目录中指定一个YAML格式的文件来实现。以下是YAML格式的配置文件样本。文件中的字段名称必须与singleServerConfigconfig对象里的字段名称相符
  1. ---
  2. singleServerConfig:
  3. #如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,
  4. #那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。
  5. #默认值:10000
  6. idleConnectionTimeout: 10000
  7. pingTimeout: 1000
  8. #同任何节点建立连接时的等待超时。时间单位是毫秒。
  9. #默认值:10000
  10. connectTimeout: 10000
  11. #等待节点回复命令的时间。该时间从命令发送成功时开始计时。
  12. #默认值:3000
  13. timeout: 3000
  14. #如果尝试达到 retryAttempts(命令失败重试次数)
  15. #仍然不能将命令发送至某个指定的节点时,将抛出错误。如果尝试在此限制之内发送成功,
  16. #则开始启用 timeout(命令等待超时) 计时
  17. #默认值:3
  18. retryAttempts: 3
  19. #在某个节点执行相同或不同命令时,连续失败failedAttempts(执行失败最大次数)时,
  20. #该节点将被从可用节点列表里清除,直到 reconnectionTimeout(重新连接时间间隔) 超时以后再次尝试。
  21. #默认值:1500
  22. retryInterval: 1500
  23. #重新连接时间间隔
  24. reconnectionTimeout: 3000
  25. #执行失败最大次数
  26. failedAttempts: 3
  27. #密码
  28. password: null
  29. #每个连接的最大订阅数量。
  30. #默认值:5
  31. subscriptionsPerConnection: 5
  32. #在Redis节点里显示的客户端名称。
  33. clientName: null
  34. #在Redis节点
  35. address: "redis://192.168.112.77:6379"
  36. #从节点发布和订阅连接的最小空闲连接数
  37. #默认值:1
  38. subscriptionConnectionMinimumIdleSize: 1
  39. #用于发布和订阅连接的连接池最大容量。连接池的连接数量自动弹性伸缩。
  40. #默认值:50
  41. subscriptionConnectionPoolSize: 50
  42. #节点最小空闲连接数
  43. #默认值:32
  44. connectionMinimumIdleSize: 32
  45. #节点连接池大小
  46. #默认值:64
  47. connectionPoolSize: 64
  48. #这个线程池数量被所有RTopic对象监听器,RRemoteService调用者和RExecutorService任务共同共享。
  49. #默认值: 当前处理核数量 * 2
  50. threads: 8
  51. #这个线程池数量是在一个Redisson实例内,被其创建的所有分布式数据类型和服务,
  52. #以及底层客户端所一同共享的线程池里保存的线程数量。
  53. #默认值: 当前处理核数量 * 2
  54. nettyThreads: 8
  55. #Redisson的对象编码类是用于将对象进行序列化和反序列化,以实现对该对象在Redis里的读取和存储。
  56. #默认值: org.redisson.codec.JsonJacksonCodec
  57. codec: !<org.redisson.codec.JsonJacksonCodec> {}
  58. #传输模式
  59. #默认值:TransportMode.NIO
  60. transportMode: "NIO"

1.2、Cluster节点配置

配置集群模式可以通过指定一个YAML格式的文件来实现。以下是YAML格式的配置文件样本。文件中的字段名称必须与clusterServersConfig和config对象里的字段名称相符。

  1. ---
  2. clusterServersConfig:
  3. #如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,
  4. #那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。
  5. #默认值:10000
  6. idleConnectionTimeout: 10000
  7. #同任何节点建立连接时的等待超时。时间单位是毫秒。
  8. #默认值:10000
  9. connectTimeout: 10000
  10. #等待节点回复命令的时间。该时间从命令发送成功时开始计时。
  11. #默认值:3000
  12. timeout: 3000
  13. #如果尝试达到 retryAttempts(命令失败重试次数)
  14. #仍然不能将命令发送至某个指定的节点时,将抛出错误。如果尝试在此限制之内发送成功,
  15. #则开始启用 timeout(命令等待超时) 计时。
  16. #默认值:3
  17. retryAttempts: 3
  18. #在某个节点执行相同或不同命令时,连续失败failedAttempts(执行失败最大次数)时,
  19. #该节点将被从可用节点列表里清除,直到 reconnectionTimeout(重新连接时间间隔) 超时以后再次尝试。
  20. #默认值:1500
  21. retryInterval: 1500
  22. #密码
  23. password: pass
  24. #每个连接的最大订阅数量。
  25. #默认值:5
  26. subscriptionsPerConnection: 5
  27. clientName: null
  28. #负载均衡算法类的选择
  29. #默认值: org.redisson.connection.balancer.RoundRobinLoadBalancer
  30. #在使用多个Elasticache Redis服务节点的环境里,可以选用以下几种负载均衡方式选择一个节点:
  31. #org.redisson.connection.balancer.WeightedRoundRobinBalancer - 权重轮询调度算法
  32. #org.redisson.connection.balancer.RoundRobinLoadBalancer - 轮询调度算法
  33. #org.redisson.connection.balancer.RandomLoadBalancer - 随机调度算法
  34. loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
  35. slaveSubscriptionConnectionMinimumIdleSize: 1
  36. slaveSubscriptionConnectionPoolSize: 50
  37. slaveConnectionMinimumIdleSize: 32
  38. slaveConnectionPoolSize: 64
  39. masterConnectionMinimumIdleSize: 32
  40. masterConnectionPoolSize: 64
  41. readMode: "SLAVE"
  42. nodeAddresses:
  43. - "redis://192.168.112.71:7001"
  44. - "redis://192.168.112.72:7001"
  45. - "redis://192.168.112.73:7001"
  46. - "redis://192.168.112.74:7001"
  47. - "redis://192.168.112.75:7001"
  48. - "redis://192.168.112.76:7001"
  49. scanInterval: 1000
  50. threads: 8
  51. nettyThreads: 8
  52. codec: !<org.redisson.codec.JsonJacksonCodec> {}
  53. "transportMode":"NIO"

2、redisson-分布式对象

上面我们集成了redission客户端,下面我们来看下他的基本使用

2.1、key操作相关

  1. 所有与Redis key相关的操作都归纳在RKeys,我们可以通过RedissonClient直接对keys进行操作。
  1. package com.itheima.redission.service;
  2. import lombok.extern.log4j.Log4j2;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.redisson.api.RBucket;
  5. import org.redisson.api.RKeys;
  6. import org.redisson.api.RedissonClient;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.stereotype.Component;
  9. /**
  10. * @ClassName KeyOpertions.java
  11. * @Description 关于key的操作
  12. */
  13. @Slf4j
  14. @Component
  15. public class KeyOpertions {
  16. @Autowired
  17. RedissonClient redissonClient;
  18. public void foundedKeys(){
  19. RBucket<Object> bucket = redissonClient.getBucket("security:aa");
  20. bucket.set("张三");
  21. //获得所有keys
  22. RKeys keys = redissonClient.getKeys();
  23. Iterable<String> keysAll = keys.getKeys();
  24. for (String key : keysAll) {
  25. log.info("获得key:"+key);
  26. }
  27. //获得所有security开头的key
  28. Iterable<String> redisKeys = keys.getKeysByPattern("security*");
  29. for (String key : redisKeys) {
  30. log.info("获得key:"+key);
  31. }
  32. }
  33. public void deleteKeys(){
  34. RBucket<String> testA = redissonClient.getBucket("testA");
  35. RBucket<String> testB = redissonClient.getBucket("testB");
  36. testA.set("张成成");
  37. testB.set("张成成女朋友");
  38. //获得所有keys
  39. RKeys keys = redissonClient.getKeys();
  40. long flag = keys.delete("testA","testB");
  41. log.info("批量删除key:testA,testB:{}",flag);
  42. testA.set("张成成");
  43. testB.set("张成成女朋友");
  44. flag = keys.deleteByPattern("test*");
  45. log.info("模糊删除key:testA,testB:{}",flag);
  46. }
  47. }

2.2、通用对象桶

  1. Redisson的分布式RBucket是一种通用对象桶可以用来存放任类型的对象,每个Redisson对象实例都会有一个与之对应的Redis数据实例,可以通过调用get***方法来取得Redis数据实例的名称(key),查看BucketOpertions类中的方法BucketOper
  1. package com.itheima.redission.service;
  2. import com.itheima.redission.pojo.AnyObject;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.redisson.api.RBucket;
  5. import org.redisson.api.RBuckets;
  6. import org.redisson.api.RedissonClient;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.stereotype.Component;
  9. import java.util.Map;
  10. import java.util.concurrent.TimeUnit;
  11. /**
  12. * @ClassName BucketOpertions.java
  13. * @Description 通用对象桶
  14. */
  15. @Slf4j
  16. @Component
  17. public class BucketOpertions {
  18. @Autowired
  19. RedissonClient redissonClient;
  20. /***
  21. * @description Bucket通用对象通
  22. */
  23. public void bucketOper(){
  24. //获得Bucket
  25. RBucket<AnyObject> anyObjectRBucket = redissonClient.getBucket("BucketOpertions");
  26. //放入一个元素
  27. AnyObject anyObject = AnyObject.builder()
  28. .name("张三")
  29. .age(19)
  30. .address("中国上海")
  31. .build();
  32. //为BucketOperTest添加元素到redis中
  33. anyObjectRBucket.set(anyObject);
  34. long timeToLive = anyObjectRBucket.remainTimeToLive();
  35. log.info("BucketOperTest存活时间:{}",timeToLive);
  36. //修改BucketOperTest的存活时间为600秒
  37. anyObjectRBucket.set(anyObject,600, TimeUnit.SECONDS);
  38. timeToLive = anyObjectRBucket.remainTimeToLive();
  39. log.info("BucketOperTest存活时间:{}",timeToLive/1000);
  40. //试着为BucketOperTest添加元素到redis中,并且存活时间为600秒
  41. boolean trySetFlag = anyObjectRBucket.trySet(anyObject, 600, TimeUnit.SECONDS);
  42. log.info("试着为BucketOperTest存储元素:{}",trySetFlag);
  43. //在holder中获取当前元素并将其替换为新值
  44. AnyObject anyObjectNew = AnyObject.builder()
  45. .name("李四")
  46. .age(20)
  47. .address("中国北京")
  48. .build();
  49. AnyObject anyObjectResult = anyObjectRBucket.getAndSet(anyObjectNew);
  50. log.info("BucketOperTest原始值:{}",anyObjectResult);
  51. anyObjectResult = anyObjectRBucket.get();
  52. log.info("BucketOperTest新添值:{}",anyObjectResult);
  53. //移除BucketOperTest
  54. boolean deleteFalg = anyObjectRBucket.delete();
  55. log.info("BucketOperTest删除:{}",deleteFalg);
  56. }
  57. /***
  58. * @description Buckets批量通用对象通
  59. */
  60. public void bucketsOper(){
  61. //获得Bucket
  62. RBucket<AnyObject> bucketA = redissonClient.getBucket("BucketOpertionsTestA");
  63. //放入一个元素
  64. AnyObject anyObject = AnyObject.builder()
  65. .name("张三")
  66. .age(19)
  67. .address("中国上海")
  68. .build();
  69. //为BucketOperTest添加元素到redis中
  70. bucketA.set(anyObject);
  71. //获得Bucket
  72. RBucket<AnyObject> bucketB = redissonClient.getBucket("BucketOpertionsTestB");
  73. bucketB.set(anyObject);
  74. //获得Buckets
  75. RBuckets buckets = redissonClient.getBuckets();
  76. //这里的兼具map的属性
  77. Map<String, AnyObject> bucketsOperMap = buckets.get("BucketOpertionsTestA", "BucketOpertionsTestB");
  78. log.info("map的元素信息:{}",bucketsOperMap);
  79. //删除所有元素
  80. buckets.delete("BucketOpertionsTestA", "BucketOpertionsTestB");
  81. }
  82. }

还可以通过RBuckets接口实现批量操作多个RBucket对象,查看BucketOpertions类中的方法BucketsOper:

  1. /***
  2. * @description Buckets批量通用对象通
  3. */
  4. public void BucketsOper(){
  5. //获得Bucket
  6. RBucket<AnyObject> bucketA=redissonClient.getBucket("BucketOperTestA");
  7. //放入一个元素
  8. AnyObject anyObject=AnyObject.builder()
  9. .name("张三")
  10. .age(19)
  11. .address("中国上海")
  12. .build();
  13. //为BucketOperTest添加元素到redis中
  14. bucketA.set(anyObject);
  15. //获得Bucket
  16. RBucket<AnyObject> bucketB=redissonClient.getBucket("BucketOperTestB");
  17. bucketB.set(anyObject);
  18. //获得Buckets
  19. RBuckets buckets=redissonClient.getBuckets();
  20. //这里的兼具map的属性
  21. Map<String, AnyObject> bucketsOperMap=buckets.get("BucketOperTestA", "BucketOperTestB");
  22. log.info("map的元素信息:{}",bucketsOperMap);
  23. //删除所有元素
  24. bucketsOperMap.clear();
  25. }

2.3、原子整长形

  1. Redisson的分布式整长形RAtomicLongjava.util.concurrent.atomic.AtomicLong对象类似
  1. package com.itheima.redission.service;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.redisson.api.RAtomicLong;
  4. import org.redisson.api.RedissonClient;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.stereotype.Component;
  7. /**
  8. * @ClassName AtomicLongOpertions.java
  9. * @Description 原子整长型操作
  10. */
  11. @Slf4j
  12. @Component
  13. public class AtomicLongOpertions {
  14. @Autowired
  15. RedissonClient redissonClient;
  16. public void atomicLongOper(){
  17. RAtomicLong atomicLongOper=redissonClient.getAtomicLong("AtomicLongOper");
  18. //添加一个从0开始的元素
  19. atomicLongOper.set(0);
  20. //获得当前元素
  21. long flag=atomicLongOper.get();
  22. log.info("获得当前元素:{}",flag);
  23. //先递增1,然后返回元素
  24. flag=atomicLongOper.incrementAndGet();
  25. log.info("先递增1,然后返回元素:{}",flag);
  26. //先获得元素,再递增1
  27. flag=atomicLongOper.getAndIncrement();
  28. log.info("先获得元素,再递增1",flag);
  29. //获得当前元素
  30. flag=atomicLongOper.get();
  31. log.info("获得当前元素:{}",flag);
  32. //先递减1,然后返回元素
  33. flag=atomicLongOper.decrementAndGet();
  34. log.info("先递减1,然后返回元素:{}",flag);
  35. //先获得元素,再递增1
  36. flag=atomicLongOper.getAndDecrement();
  37. log.info("先获得元素,再递减1",flag);
  38. //获得当前元素
  39. flag=atomicLongOper.get();
  40. log.info("获得当前元素:{}",flag);
  41. //删除元素
  42. boolean delete=atomicLongOper.delete();
  43. log.info("删除当前元素:{}",delete);
  44. //添加并且获得元素,如果想批量递减可以传入负数
  45. flag=atomicLongOper.addAndGet(2);
  46. log.info("添加并且获得元素:{}",flag);
  47. //获得并且删除元素
  48. flag=atomicLongOper.getAndDelete();
  49. log.info("获得并且删除元素:{}",flag);
  50. }
  51. }

2.4、原子双精度浮点

  1. Redisson还提供了分布式原子双精度浮点RAtomicDouble弥补了Java自身的不足
  1. package com.itheima.redission.service;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.redisson.api.RAtomicDouble;
  4. import org.redisson.api.RedissonClient;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.stereotype.Component;
  7. /**
  8. * @ClassName AtomicDoubleOpertions.java
  9. * @Description 原子浮点型操作
  10. */
  11. @Slf4j
  12. @Component
  13. public class AtomicDoubleOpertions {
  14. @Autowired
  15. RedissonClient redissonClient;
  16. public void atomicDoubleOper(){
  17. RAtomicDouble atomicDoubleOper=redissonClient.getAtomicDouble("AtomicDoubleOper");
  18. //添加一个从2.0开始的元素
  19. atomicDoubleOper.set(2.0D);
  20. //获得当前元素
  21. double flag=atomicDoubleOper.get();
  22. log.info("获得当前元素:{}",flag);
  23. //先递增1,然后返回元素
  24. flag=atomicDoubleOper.incrementAndGet();
  25. log.info("先递增1,然后返回元素:{}",flag);
  26. //先获得元素,再递增1
  27. flag=atomicDoubleOper.getAndIncrement();
  28. log.info("先获得元素,再递增1",flag);
  29. //获得当前元素
  30. flag=atomicDoubleOper.get();
  31. log.info("获得当前元素:{}",flag);
  32. //先递减1,然后返回元素
  33. flag=atomicDoubleOper.decrementAndGet();
  34. log.info("先递减1,然后返回元素:{}",flag);
  35. //先获得元素,再递增1
  36. flag=atomicDoubleOper.getAndDecrement();
  37. log.info("先获得元素,再递减1",flag);
  38. //获得当前元素
  39. flag=atomicDoubleOper.get();
  40. log.info("获得当前元素:{}",flag);
  41. //删除元素
  42. boolean delete=atomicDoubleOper.delete();
  43. log.info("删除当前元素:{}",delete);
  44. //添加并且获得元素
  45. flag=atomicDoubleOper.addAndGet(2);
  46. log.info("添加并且获得元素:{}",flag);
  47. //获得并且删除元素
  48. flag=atomicDoubleOper.getAndDelete();
  49. log.info("获得并且删除元素:{}",flag);
  50. }
  51. }

3、redisson-分布式集合

3.1、RMap对象

  1. 基于RedisRedisson的分布式映射结构的RMap对象实现了`java.util.concurrent.ConcurrentMap`接口和`java.util.Map`接口。与HashMap不同的是RMap保持了元素的插入顺序。该对象的最大容量受Redis限制,最大元素数量是4 294 967 295

image.png

  1. package com.itheima.redission.service;
  2. import com.itheima.redission.pojo.AnyObject;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.redisson.api.RMap;
  5. import org.redisson.api.RedissonClient;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.stereotype.Component;
  8. import java.util.Collection;
  9. import java.util.HashMap;
  10. import java.util.Map;
  11. import java.util.Set;
  12. /**
  13. * @ClassName RMapOpertions.java
  14. * @Description hash表操作
  15. */
  16. @Slf4j
  17. @Component
  18. public class RMapOpertions {
  19. @Autowired
  20. RedissonClient redissonClient;
  21. public void rMapOper(){
  22. //获得hash表,这里RMapOpertions为主key
  23. RMap<String, AnyObject> userInfo=redissonClient.getMap("userInfo");
  24. AnyObject anyObjectA=AnyObject.builder().id("1").name("嬴政").age(22).address("秦朝").build();
  25. AnyObject anyObjectB=AnyObject.builder().id("2").name("李斯").age(22).address("秦朝").build();
  26. AnyObject anyObjectC=AnyObject.builder().id("3").name("孙悟空").age(22).address("唐").build();
  27. //添加元素,返回的值为之前hash表中的值
  28. userInfo.put(anyObjectA.getId(),anyObjectA);
  29. userInfo.put(anyObjectB.getId(),anyObjectB);
  30. userInfo.put(anyObjectC.getId(),anyObjectC);
  31. //获得userInfo中所有的key
  32. Set<String> keySet=userInfo.readAllKeySet();
  33. log.info("获得userInfo中所有的key:{}",keySet.toString());
  34. //获得userInfo中所有的values
  35. Collection<AnyObject> anyObjects=userInfo.readAllValues();
  36. log.info("获得userInfo中所有的值:{}",anyObjects.toString());
  37. //获得userInfo中所有的元素对象
  38. Set<Map.Entry<String, AnyObject>> entries=userInfo.readAllEntrySet();
  39. log.info("获得userInfo中所有的元素对象:{}",entries.toString());
  40. userInfo.clear();
  41. //快速添加元素,与put的不同是不返回值,且添加速度快
  42. userInfo.fastPut(anyObjectA.getId(),anyObjectA);
  43. userInfo.fastPut(anyObjectB.getId(),anyObjectB);
  44. userInfo.fastPut(anyObjectC.getId(),anyObjectC);
  45. userInfo.clear();
  46. //批量添加
  47. Map<String,AnyObject> map=new HashMap<>();
  48. map.put(anyObjectA.getId(),anyObjectA);
  49. map.put(anyObjectB.getId(),anyObjectB);
  50. map.put(anyObjectC.getId(),anyObjectC);
  51. userInfo.putAll(map);
  52. //根据辅key获得元素,【和map中获得元素一样】
  53. AnyObject anyObjectResult=userInfo.get(anyObjectA.getId());
  54. log.info("根据辅key获得元素对象:{}",anyObjectResult.toString());
  55. //试着添加元素,如果元素key存在则不做任何修改,,如果元素key不存在则做修改,
  56. //返回结果为之前值【如果返回null,表明之前每页存储过元素】
  57. AnyObject anyObjectD=AnyObject.builder()
  58. .id("4").name("如来佛").age(1000000).address("上古").build();
  59. AnyObject anyObject=userInfo.putIfAbsent(anyObjectD.getId(), anyObjectD);
  60. //清除所有元素
  61. userInfo.clear();
  62. }
  63. }

3.2、RMapCache对象

  1. Redisson的分布式的RMapCache象在基于RMap的前提下实现了针对单个元素的淘汰机制。同时仍然保留了元素的插入顺序。由于RMapCache是基于RMap实现的,使它同时继承了java.util.concurrent.ConcurrentMap接口和java.util.Map接口。
  1. package com.itheima.redission.service;
  2. import com.itheima.redission.pojo.AnyObject;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.redisson.api.RMap;
  5. import org.redisson.api.RMapCache;
  6. import org.redisson.api.RedissonClient;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.stereotype.Component;
  9. import java.util.Collection;
  10. import java.util.Map;
  11. import java.util.Set;
  12. import java.util.concurrent.TimeUnit;
  13. /**
  14. * @ClassName RMapCacheOpertions.java
  15. * @Description 带淘汰机制的hash表操作
  16. */
  17. @Slf4j
  18. @Component
  19. public class RMapCacheOpertions {
  20. @Autowired
  21. RedissonClient redissonClient;
  22. public void rMapCache(){
  23. //获得hash表,这里RMapOpertions为主key
  24. RMapCache<String, AnyObject> userInfo=redissonClient.getMapCache("userInfo");
  25. AnyObject anyObjectA=AnyObject.builder().id("1").name("嬴政").age(22).address("秦朝").build();
  26. AnyObject anyObjectB=AnyObject.builder().id("2").name("李斯").age(22).address("秦朝").build();
  27. AnyObject anyObjectC=AnyObject.builder().id("3").name("孙悟空").age(22).address("唐").build();
  28. //添加元素,返回的值为之前hash表中的值,并且为每个子元素添加过期时间
  29. userInfo.put(anyObjectA.getId(),anyObjectA,20, TimeUnit.SECONDS);
  30. userInfo.put(anyObjectB.getId(),anyObjectB,20, TimeUnit.SECONDS);
  31. userInfo.put(anyObjectC.getId(),anyObjectC,20, TimeUnit.SECONDS);
  32. //获得userInfo中所有的key
  33. Set<String> keySet=userInfo.readAllKeySet();
  34. log.info("获得userInfo中所有的key:{}",keySet.toString());
  35. //获得userInfo中所有的values
  36. Collection<AnyObject> anyObjects=userInfo.readAllValues();
  37. log.info("获得userInfo中所有的值:{}",anyObjects.toString());
  38. //获得userInfo中所有的元素对象
  39. Set<Map.Entry<String, AnyObject>> entries=userInfo.readAllEntrySet();
  40. log.info("获得userInfo中所有的元素对象:{}",entries.toString());
  41. userInfo.clear();
  42. //其他操作与RMap类似,这里就不再操作
  43. }
  44. }

==注意:==目前的Redis自身并不支持散列(Hash)当中的元素淘汰,因此所有过期元素都是通过org.redisson.EvictionScheduler实例来实现定期清理的。为了保证资源的有效利用,每次运行最多清理300个过期元素。任务的启动时间将根据上次实际清理数量自动调整,间隔时间趋于1秒到1小时之间。比如该次清理时删除了300条元素,那么下次执行清理的时间将在1秒以后(最小间隔时间)。一旦该次清理数量少于上次清理数量,时间间隔将增加1.5倍

4、redisson-分布式锁

4.1、原理分析

image.png

  • 加锁机制
    线程去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。
    线程去获取锁,获取失败: 一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis
  • WatchDog自动延期机制
    第一种情况:在一个分布式环境下,假如一个线程获得锁后,突然服务器宕机了,那么这个时候在一定时间后这个锁会自动释放,你也可以设置锁的有效时间(不设置默认30秒),这样的目的主要是防止死锁的发生
    第二种情况:线程A业务还没有执行完,时间就过了,线程A 还想持有锁的话,就会启动一个watch dog后台线程,不断的延长锁key的生存时间
  • lua脚本
    主要是如果你的业务逻辑复杂的话,通过封装在lua脚本中发送给redis,而且redis是单线程的,这样就保证这段复杂业务逻辑执行的原子性

4.2、基本使用

  1. 很明显RLock是继承Lock锁,所以他有Lock锁的所有特性,比如lockunlocktrylock等特性,同时它还有很多新特性:强制锁释放,带有效期的锁,。
  1. public interface RLock {
  2. //----------------------Lock接口方法-----------------------
  3. /**
  4. * 加锁 锁的有效期默认30秒
  5. */
  6. void lock();
  7. /**
  8. * 加锁 可以手动设置锁的有效时间
  9. *
  10. * @param leaseTime 锁有效时间
  11. * @param unit 时间单位 小时、分、秒、毫秒等
  12. */
  13. void lock(long leaseTime, TimeUnit unit);
  14. /**
  15. * tryLock()方法是有返回值的,用来尝试获取锁,
  16. * 如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false .
  17. */
  18. boolean tryLock();
  19. /**
  20. * tryLock(long waitTime, TimeUnit unit)方法和tryLock()方法是类似的,
  21. * 只不过区别在于这个方法在拿不到锁时会等待一定的时间,
  22. * 在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
  23. *
  24. * @param time 等待时间
  25. * @param unit 时间单位 小时、分、秒、毫秒等
  26. */
  27. boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException;
  28. /**
  29. * 比上面多一个参数,多添加一个锁的有效时间
  30. *
  31. * @param waitTime 等待时间
  32. * @param leaseTime 锁有效时间
  33. * @param unit 时间单位 小时、分、秒、毫秒等
  34. */
  35. boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
  36. /**
  37. * 解锁
  38. */
  39. void unlock();
  40. }

lock():此方法为加锁,但是锁的有效期采用默认30秒,如果主线程未释放,且当前锁未调用unlock方法,则进入到watchDog机制,如果主线程未释放,且当前锁调用unlock方法,则直接释放锁

  1. public void lock() throws InterruptedException{
  2. log.info("线程:{},进入方法",Thread.currentThread().getName());
  3. RLock rLock=redissonClient.getLock("lock");
  4. //加锁:锁的有效期默认30秒
  5. rLock.lock();
  6. long timeToLive=rLock.remainTimeToLive();
  7. log.info("线程:{},获得锁,锁存活时间:{}S",Thread.currentThread().getName(),timeToLive/1000);
  8. //休眠一下
  9. Thread.sleep(2000);
  10. //如果主线程未释放,且当前锁未调用unlock方法,则进入到watchDog机制
  11. //如果主线程未释放,且当前锁调用unlock方法,则直接释放锁
  12. rLock.unlock();
  13. log.info("线程:{},释放锁",Thread.currentThread().getName());
  14. }

lock(long leaseTime, TimeUnit unit):可以手动设置锁的有效时间,如果主线程未释放,且当前锁未调用unlock方法,则锁到期后会自动释放,如果主线程未释放,且当前锁调用unlock方法,则直接释放锁

  1. public void lockLaseTime() throws InterruptedException{
  2. log.info("线程:{},进入方法",Thread.currentThread().getName());
  3. RLock rLock=redissonClient.getLock("lockLaseTime");
  4. //加锁 上面是默认30秒,
  5. //这里可以手动设置锁的有效时间,锁到期后会自动释放的
  6. rLock.lock(10,TimeUnit.SECONDS);
  7. long timeToLive=rLock.remainTimeToLive();
  8. log.info("线程:{},获得锁,锁存活时间:{}S",Thread.currentThread().getName(),timeToLive/1000);
  9. //休眠一下
  10. Thread.sleep(2000);
  11. //如果主线程未释放,且当前锁未调用unlock方法,则锁到期后会自动释放
  12. //如果主线程未释放,且当前锁调用unlock方法,则直接释放锁
  13. rLock.unlock();
  14. log.info("线程:{},释放锁",Thread.currentThread().getName());
  15. }

tryLock():用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,如果主线程未释放,且当前锁未调用unlock方法,则进入到watchDog机制,如果主线程未释放,且当前锁调用unlock方法,则直接释放锁

  1. public void tryLock() throws InterruptedException {
  2. log.info("线程:{},进入方法",Thread.currentThread().getName());
  3. RLock rLock=redissonClient.getLock("tryLock");
  4. //tryLock()方法是有返回值的,它表示用来尝试获取锁,
  5. //如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false .
  6. boolean flag=rLock.tryLock();
  7. if (flag){
  8. long timeToLive=rLock.remainTimeToLive();
  9. log.info("线程:{},获得锁,锁存活时间:{}S,加锁状态:{}",Thread.currentThread().getName(),timeToLive/1000,flag);
  10. //休眠一下
  11. Thread.sleep(2000);
  12. //如果主线程未释放,且当前锁未调用unlock方法,则进入到watchDog机制
  13. //如果主线程未释放,且当前锁调用unlock方法,则直接释放锁
  14. rLock.unlock();
  15. log.info("线程:{},释放锁",Thread.currentThread().getName());
  16. }else {
  17. log.info("线程:{},获得锁失败",Thread.currentThread().getName());
  18. }
  19. }

tryLock(long time, TimeUnit unit):tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间, 在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true,如果主线程未释放,且当前锁未调用unlock方法,则进入到watchDog机制如果主线程未释放,且当前锁调用unlock方法,则直接释放锁

  1. public void tryLockWaitTime() throws InterruptedException {
  2. log.info("线程:{},进入方法",Thread.currentThread().getName());
  3. RLock rLock=redissonClient.getLock("tryLockWaitTime");
  4. //tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,
  5. //只不过区别在于这个方法在拿不到锁时会等待一定的时间,
  6. //在时间期限之内如果还拿不到锁,就返回false如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
  7. boolean flag=rLock.tryLock(6, TimeUnit.SECONDS);
  8. if (flag){
  9. long timeToLive=rLock.remainTimeToLive();
  10. log.info("线程:{},获得锁,锁存活时间:{}S,加锁状态:{}",Thread.currentThread().getName(),timeToLive/1000,flag);
  11. //休眠一下
  12. Thread.sleep(10000);
  13. //如果主线程未释放,且当前锁未调用unlock方法,则进入到watchDog机制
  14. //如果主线程未释放,且当前锁调用unlock方法,则直接释放锁
  15. rLock.unlock();
  16. log.info("线程:{},释放锁",Thread.currentThread().getName());
  17. }else {
  18. log.info("线程:{},获得锁失败",Thread.currentThread().getName());
  19. }
  20. }

tryLock(long waitTime, long leaseTime, TimeUnit unit):比上面多一个参数,多添加一个锁的有效时间

  1. public void tryLockleasTime() throws InterruptedException {
  2. log.info("线程:{},进入方法",Thread.currentThread().getName());
  3. RLock rLock = redissonClient.getLock("tryLockleasTime");
  4. //比上面多一个参数,多添加一个锁的有效时间
  5. boolean flag = rLock.tryLock(11,10, TimeUnit.SECONDS);
  6. if (flag){
  7. long timeToLive = rLock.remainTimeToLive();
  8. log.info("线程:{},获得锁,锁存活时间:{}S,加锁状态:{}",Thread.currentThread().getName(),timeToLive/1000,flag);
  9. //休眠一下
  10. Thread.sleep(6000);
  11. //如果主线程未释放,且当前锁未调用unlock方法,则锁到期后会自动释放的
  12. //如果主线程未释放,且当前锁调用unlock方法,则直接释放锁
  13. rLock.unlock();
  14. log.info("线程:{},释放锁",Thread.currentThread().getName());
  15. }else {
  16. log.info("线程:{},获得锁失败",Thread.currentThread().getName());
  17. }
  18. }

第二章 点餐平台-开桌、主体信息、订单详情

下面我们进入的餐掌柜的核心业务—点餐,点餐我们采用的是小程序开发,用户通过扫码进入系统,然后完成点餐、下单等操作

image.png

1、功能区拆解

下面我们首先看下整个的业务流程:

image.png

具体流程如下:

  • 用户通过二维码扫码,进入门店点餐系统
  • 扫码之后,查询店铺主体信息,查询桌台是否开台
  • 已开台:查询当前桌台订单信息【包括可核算订单项和购物车订单项】
  • 未开台:选择就餐人数,创建订单
  • 查询桌台订单信息,返回主体信息

2、数据库结构

image.png

上图为整个桌与相关管理表的关系,各位需要注意,订单项的信息存放在2个位置:

  • Mysql:用户已经下单,可以作为核算的订单项
  • Redis:用户加入购物车,但是未下单的作为临时存储的订单项
  • 在用户执行下单时,我们会吧Redis的购物车订单项会合并到可核算订单项中

3、功能开发

image.png

model-shop-applet模块为H5程序微服务的生产,其核心uml类图如下:

image.png

3.1、桌台是否开台

桌台是否开台的前提条件:

  • 当前桌台处于空闲状态且无【待付款、支付中】订单,认为桌台未开台,可以使用
  • 当前桌台存在处于【待付款、支付中】的订单,认为当前桌台已开台
  • AppletController
    传入tableId直接调用appletFace.isOpen(tableId)返回已开台:true,未开台:false
  1. @DubboReference(version = "${dubbo.application.version}",check = false)
  2. AppletFace appletFace;
  3. @GetMapping("is-open/{tableId}")
  4. @ApiOperation(value = "查询是否开桌",notes = "是否开桌,已开台:进入继续点餐流程,未开台:进入开台流程")
  5. @ApiImplicitParam(paramType = "path",name = "tableId",value = "桌台",dataType = "Long")
  6. public ResponseWrap<Boolean> isOpen(@PathVariable("tableId") Long tableId) {
  7. Boolean isOpen = appletFace.isOpen(tableId);
  8. return ResponseWrapBuild.build(TableEnum.SUCCEED,isOpen);
  9. }
  • AppletFace
  1. /***
  2. * @description 是否开桌,已开台:进入继续点餐流程,未开台:进入开台流程
  3. * @param tableId 桌台Id
  4. * @return Boolean
  5. */
  6. Boolean isOpen(Long tableId)throws ProjectException;
  • AppletFaceImpl

    1、查询桌台信息,是否为空闲

    2、是否已经有【待支付、支付中】订单存在

  1. /**
  2. * @ClassName AppletFaceImpl.java
  3. * @Description 小程序H5实现
  4. */
  5. @Slf4j
  6. @DubboService(version = "${dubbo.application.version}", timeout = 5000,
  7. methods = {
  8. @Method(name = "isOpen", retries = 2)
  9. }
  10. )
  11. public class AppletFaceImpl implements AppletFace {
  12. @Autowired
  13. IOrderService orderService;
  14. @Autowired
  15. ITableService tableService;
  16. @Override
  17. public Boolean isOpen(Long tableId) throws ProjectException{
  18. try {
  19. //1、查询桌台信息,是否为使用中
  20. Table table = tableService.getById(tableId);
  21. Boolean flagTableStatus = table.getTableStatus().equals(SuperConstant.USE);
  22. //2、是否已经有【待支付、支付中】订单存在
  23. OrderVo orderVoResult = orderService.findOrderByTableId(tableId);
  24. Boolean flagOrderVo = !EmptyUtil.isNullOrEmpty(orderVoResult);
  25. if (flagTableStatus||flagOrderVo){
  26. return true;
  27. }
  28. return false;
  29. }catch (Exception e){
  30. log.error("查询桌台信息异常:{}", ExceptionsUtil.getStackTraceAsString(e));
  31. throw new ProjectException(TableEnum.SELECT_TABLE_FAIL);
  32. }
  33. }
  34. }
  • OrderServiceImpl

    1、查询当前处于待支付,付款中的有效订单

  1. @Override
  2. public OrderVo findOrderByTableId(Long tableId) {
  3. QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
  4. queryWrapper.lambda().eq(Order::getTableId,tableId);
  5. queryWrapper.lambda().eq(Order::getEnableFlag,SuperConstant.YES);
  6. queryWrapper.lambda().and(wrapper->wrapper
  7. .eq(Order::getOrderState,SuperConstant.DFK)
  8. .or()
  9. .eq(Order::getOrderState,SuperConstant.FKZ));
  10. Order order = getOne(queryWrapper);
  11. return BeanConv.toBean(order,OrderVo.class);
  12. }

3.2、相关主体信息

  1. 无论用户是首次开台或者是进入已经开台的桌台,都需要展现品牌、门店、桌台、菜品分类、菜品、等相关的信息,这里包装了一个服务接口一次性把所有的数据都拿到。

image.png

  • AppletController
    AppletInfoVo对象包所有相关信息,注意:品牌图片信息、菜品图片口味信息需要调用通用服务获得
  1. @DubboReference(version = "${dubbo.application.version}",check = false)
  2. AppletFace appletFace;
  3. @GetMapping("table-appletInfo/{tableId}")
  4. @ApiOperation(value = "查询桌台相关主体信息",notes = "查询桌台相关主体信息:品牌、门店、菜品、口味、分类等")
  5. @ApiImplicitParam(paramType = "path",name = "tableId",value = "桌台Id",dataType = "Long")
  6. public ResponseWrap<AppletInfoVo> findAppletInfoVoByTableId(@PathVariable("tableId") Long tableId){
  7. //AppletInfoVo对象包所有相关信息,注意:品牌图片信息、菜品图片口味信息需要调用通用服务获得
  8. AppletInfoVo appletInfoVo = appletFace.findAppletInfoVoByTableId(tableId);
  9. return ResponseWrapBuild.build(TableEnum.SUCCEED,appletInfoVo);
  10. }
  • AppletFace
  1. /***
  2. * @description 查询桌台相关主体信息
  3. * @param tableId 桌台Id
  4. * @return AppletInfoVo
  5. */
  6. AppletInfoVo findAppletInfoVoByTableId(Long tableId);
  • AppletFaceImpl

    1、查询桌台

    2、查询门店

    3、查询品牌

    1. 3.1、处理品牌图片

    4、查询分类

    5、查询菜品

    6、查询菜品口味、图片信息

    1. 6.1、口味与数字字典中间表信息
    2. 6.2、构建数字字典dataKeys
    3. 6.3RPC查询数字字典口味信息
    4. 6.4RPC查询附件信息

    7、构建返回对象

  1. @Autowired
  2. ITableService tableService;
  3. @Autowired
  4. ICategoryService categoryService;
  5. @Autowired
  6. IDishService dishService;
  7. @Autowired
  8. IDishFlavorService dishFlavorService;
  9. @Autowired
  10. IBrandService brandService;
  11. @Autowired
  12. IStoreService storeService;
  13. @Override
  14. public AppletInfoVo findAppletInfoVoByTableId(Long tableId)throws ProjectException {
  15. try {
  16. //1、查询桌台信息
  17. Table table = tableService.getById(tableId);
  18. TableVo tableVo = BeanConv.toBean(table, TableVo.class);
  19. //2、查询门店
  20. Store store = storeService.getById(table.getStoreId());
  21. StoreVo storeVo = BeanConv.toBean(store, StoreVo.class);
  22. //3、查询品牌
  23. Brand brand = brandService.getById(store.getBrandId());
  24. BrandVo brandVo = BeanConv.toBean(brand, BrandVo.class);
  25. //3.1、处理品牌图片
  26. List<AffixVo> affixVoListBrand = affixFace.findAffixVoByBusinessId(brandVo.getId());
  27. brandVo.setAffixVo(affixVoListBrand.get(0));
  28. //4、查询分类
  29. List<Category> categorys = categoryService.findCategoryVoByStoreId(table.getStoreId());
  30. List<CategoryVo> categoryVoList = BeanConv.toBeanList(categorys, CategoryVo.class);
  31. //5、查询菜品
  32. List<Dish> dishs = dishService.findDishVoByStoreId(table.getStoreId());
  33. List<DishVo> dishVos = BeanConv.toBeanList(dishs, DishVo.class);
  34. //6、查询菜品口味、图片信息
  35. dishVos.forEach(dishVo->{
  36. //6.1、口味与数字字典中间表信息
  37. List<DishFlavor> dishFlavors = dishFlavorService.findDishFlavorByDishId(dishVo.getId());
  38. List<DishFlavorVo> dishFlavorVos = BeanConv.toBeanList(dishFlavors, DishFlavorVo.class);
  39. dishVo.setDishFlavorVos(dishFlavorVos);
  40. //6.2、构建数字字典dataKeys
  41. List<String> dataKeys = dishFlavorVos.stream()
  42. .map(DishFlavorVo::getDataKey).collect(Collectors.toList());
  43. //6.3、RPC查询数字字典口味信息
  44. List<DataDictVo> valueByDataKeys = dataDictFace.findValueByDataKeys(dataKeys);
  45. dishVo.setDataDictVos(valueByDataKeys);
  46. //6.4、RPC查询附件信息
  47. List<AffixVo> affixVoListDish = affixFace.findAffixVoByBusinessId(dishVo.getId());
  48. dishVo.setAffixVo(affixVoListDish.get(0));
  49. });
  50. //7、构建返回对象
  51. AppletInfoVo appletInfoVo = AppletInfoVo.builder()
  52. .tableVo(tableVo)
  53. .storeVo(storeVo)
  54. .brandVo(brandVo)
  55. .categoryVos(categoryVoList)
  56. .dishVos(dishVos)
  57. .build();
  58. return appletInfoVo;
  59. } catch (Exception e) {
  60. log.error("查询桌台相关主体信息异常:{}", ExceptionsUtil.getStackTraceAsString(e));
  61. throw new ProjectException(TableEnum.SELECT_TABLE_FAIL);
  62. }
  63. }
  • IDishService
  1. /***
  2. * @description 查询店铺下所有起售且有效菜品
  3. * @param storeId
  4. * @return List<Dish>
  5. */
  6. List<Dish> findDishVoByStoreId(Long storeId);
  • DishServiceImpl
  1. @Override
  2. public List<Dish> findDishVoByStoreId(Long storeId) {
  3. LambdaQueryWrapper<Dish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
  4. lambdaQueryWrapper.eq(Dish::getStoreId,storeId)
  5. .eq(Dish::getEnableFlag,SuperConstant.YES)
  6. .eq(Dish::getDishStatus,SuperConstant.YES);
  7. return list(lambdaQueryWrapper);
  8. }

3.3、用户开桌操作

调用【3.1桌台是否开台】接口后,如果桌台未开台,可以调用用户开桌操作接口,指定就餐人数进行开台操作,开台操作主要完成:

1、为桌台创建【待支付】订单

2、修改桌台状态【空闲——>开桌】

image.png

  • AppletController
    传入桌台ID和就餐人数,进行开台操作
  1. @PostMapping("open-table/{tableId}/{personNumbers}")
  2. @ApiOperation(value = "开桌操作",notes = "未开桌:选择人数创建订单")
  3. @ApiImplicitParams({
  4. @ApiImplicitParam(paramType = "path",name = "tableId",value = "桌台",dataType = "Long"),
  5. @ApiImplicitParam(paramType = "path",name = "personNumbers",value = "就餐人数",dataType = "Integer"),
  6. })
  7. public ResponseWrap<OrderVo> openTable(
  8. @PathVariable("tableId") Long tableId,
  9. @PathVariable("personNumbers") Integer personNumbers) {
  10. OrderVo orderVoResult = appletFace.openTable(tableId,personNumbers);
  11. return ResponseWrapBuild.build(BrandEnum.SUCCEED,orderVoResult);
  12. }
  • AppletFace
  1. /***
  2. * @description 未开桌:选择人数创建订单
  3. * @param tableId 桌台Id
  4. * @param personNumbers 就餐人数
  5. * @return Boolean
  6. */
  7. OrderVo openTable(Long tableId,Integer personNumbers)throws ProjectException;
  • AppletFaceImpl
    幂等性:是指无论调用多少次都不会有不同结果的 HTTP 方法。不管你调用一次,还是调用一百次,一千次,结果都是相同的。

    1、开台状态定义

    2、锁定桌台,防止并发重复创建订单

    3、幂等性:再次查询桌台订单情况

    4、未开台,为桌台创建当订单

    1. 4.1、查询桌台信息
    2. 4.2、构建订单

    5、修改桌台状态为使用中

    6、订单处理:处理可核算订单项和购物车订单项,可调用桌台订单显示接口

  1. @Override
  2. @Transactional
  3. public OrderVo openTable(Long tableId,Integer personNumbers) throws ProjectException {
  4. //1、开台状态定义
  5. boolean flag = true;
  6. //2、锁定桌台,防止并发重复创建订单
  7. String key = AppletCacheConstant.OPEN_TABLE_LOCK+tableId;
  8. RLock lock = redissonClient.getLock(key);
  9. try {
  10. if(lock.tryLock(AppletCacheConstant.REDIS_WAIT_TIME,
  11. AppletCacheConstant.REDIS_LEASETIME,
  12. TimeUnit.SECONDS)){
  13. //3、幂等性:再次查询桌台订单情况
  14. OrderVo orderVoResult = orderService.findOrderByTableId(tableId);
  15. //4、未开台,为桌台创建当订单
  16. if (EmptyUtil.isNullOrEmpty(orderVoResult)){
  17. //4.1、查询桌台信息
  18. Table table = tableService.getById(tableId);
  19. //4.2、构建订单
  20. Order order = Order.builder()
  21. .tableId(tableId)
  22. .tableName(table.getTableName())
  23. .storeId(table.getStoreId())
  24. .areaId(table.getAreaId())
  25. .enterpriseId(table.getEnterpriseId())
  26. .orderNo((Long) identifierGenerator.nextId(tableId))
  27. .orderState(TradingConstant.DFK)
  28. .isRefund(SuperConstant.NO)
  29. .refund(new BigDecimal(0))
  30. .discount(new BigDecimal(10))
  31. .personNumbers(personNumbers)
  32. .reduce(new BigDecimal(0))
  33. .useScore(0)
  34. .acquireScore(0l)
  35. .build();
  36. orderService.save(order);
  37. //5、修改桌台状态为使用中
  38. TableVo tableVo = TableVo.builder()
  39. .id(tableId)
  40. .tableStatus(SuperConstant.USE).build();
  41. tableService.updateTable(tableVo);
  42. }
  43. }
  44. //6、订单处理:处理可核算订单项和购物车订单项,可调用桌台订单显示接口
  45. return showOrderVoforTable(tableId);
  46. }catch (Exception e){
  47. log.error("开桌操作异常:{}", ExceptionsUtil.getStackTraceAsString(e));
  48. throw new ProjectException(TableEnum.OPEN_TABLE_FAIL);
  49. }finally {
  50. lock.unlock();
  51. }
  52. }

3.4、菜品信息详情

用户调用【3.2相关主体信息】后,可以点击菜品。查询菜品详情,并且选择自己的菜品口味,效果图如下:

image.png

  • AppletController

    1、查询菜品信息,注意:菜品图片口味信息需要调用通用服务获得

    2、处理菜品口味

    3、处理菜品图片

    4、封装返回结果

  1. @PostMapping("dish-details/{dishId}")
  2. @ApiOperation(value = "查询菜品详情",notes = "显示菜品详情,包括口味")
  3. @ApiImplicitParam(paramType = "path",name = "dishId",value = "菜品Id",dataType = "菜品Id")
  4. public ResponseWrap<DishVo> findDishVoById(@PathVariable("dishId") Long dishId) {
  5. //查询菜品信息,注意:菜品图片口味信息需要调用通用服务获得
  6. DishVo dishVo = appletFace.findDishVoById(dishId);
  7. //封装返回结果
  8. return ResponseWrapBuild.build(BrandEnum.SUCCEED,dishVo);
  9. }
  • AppletFace
  1. /***
  2. * @description 查询菜品详情
  3. * @return
  4. * @return: com.itheima.restkeeper.req.DishVo
  5. */
  6. DishVo findDishVoById(Long dishId) throws ProjectException;
  • AppletFaceImpl

    1、查询菜品信息,注意:菜品图片口味信息需要调用通用服务获得

    2、查询菜品口味

    3、处理菜品口味【数字字典】

    4、处理菜品图片

  1. @Override
  2. public DishVo findDishVoById(Long dishId)throws ProjectException {
  3. try {
  4. //1、查询菜品,注意:菜品图片口味信息需要调用通用服务获得
  5. Dish dish = dishService.getById(dishId);
  6. //2、查询菜品口味
  7. List<DishFlavor> dishFlavors = dishFlavorService.findDishFlavorByDishId(dishId);
  8. DishVo dishVo = BeanConv.toBean(dish, DishVo.class);
  9. //3、处理菜品口味【数字字典】
  10. List<DishFlavorVo> dishFlavorVos = BeanConv.toBeanList(dishFlavors,DishFlavorVo.class);
  11. List<String> DataKeys = dishFlavorVos.stream()
  12. .map(DishFlavorVo::getDataKey).collect(Collectors.toList());
  13. List<DataDictVo> valueByDataKeys = dataDictFace.findValueByDataKeys(DataKeys);
  14. dishVo.setDataDictVos(valueByDataKeys);
  15. //4、处理菜品图片
  16. List<AffixVo> affixVoListDish = affixFace.findAffixVoByBusinessId(dishVo.getId());
  17. dishVo.setAffixVo(affixVoListDish.get(0));
  18. return dishVo ;
  19. }catch (Exception e){
  20. log.error("查询菜品详情异常:{}", ExceptionsUtil.getStackTraceAsString(e));
  21. throw new ProjectException(DishEnum.SELECT_DISH_FAIL);
  22. }
  23. }

3.5、桌台订单信息

如果已经开桌,进入H5订单首页后,我们需要 拿桌台的订单项信息,订单项存储在2个存储中:

1、可核算订单项【已下单可结算】:MySQL

2、购物车订单项【已加入购物车未下单】:Redis

image.png

功能效果如图所示:

image.png

  • AppletController

    已开桌:查询当前桌台订单信息【包括可核算订单项和购物车订单项】

  1. @PostMapping("show-ordervo-table/{tableId}")
  2. @ApiOperation(value = "查询桌台订单信息",notes = "已开桌:查询当前桌台订单信息【包括可核算订单项和购物车订单项】")
  3. @ApiImplicitParam(paramType = "path",name = "tableId",value = "桌台",dataType = "Long")
  4. public ResponseWrap<OrderVo> showOrderVoforTable(@PathVariable("tableId") Long tableId) {
  5. OrderVo orderVoResult = appletFace.showOrderVoforTable(tableId);
  6. return ResponseWrapBuild.build(BrandEnum.SUCCEED,orderVoResult);
  7. }
  • AppletFace
  1. /***
  2. * @description 已开桌:查询当前桌台订单信息【包括可核算订单项和购物车订单项】
  3. * @param tableId 桌台ID
  4. * @return
  5. */
  6. OrderVo showOrderVoforTable(Long tableId) throws ProjectException;
  7. /***
  8. * @description 处理当前订单中订单项
  9. * 从DB中查询当前订单可核算订单项
  10. * 从redis查询当前订单购物车订单项
  11. * @param orderVo 订单信息
  12. * @return
  13. */
  14. OrderVo handlerOrderVo(OrderVo orderVo) throws ProjectException;
  15. /***
  16. * @description 订单项计算
  17. * @param orderItemVos 需要计算的订单项
  18. * @return
  19. * @return: java.math.BigDecimal
  20. */
  21. BigDecimal reducePriceHandler(List<OrderItemVo> orderItemVos ) throws ProjectException;
  • AppletFaceImpl

    1、查询MySQL:可核算订单项

    2、可核算订单项总金额

    1. 2.1、处理空可核算订单项
    2. 2.2、计算可核算订单项总金额

    3、查询redis:购物车订单项

    1. 3.1、计算购物车订单项总金额

    4、构建订单信息

  1. @Override
  2. public OrderVo showOrderVoforTable(Long tableId) throws ProjectException {
  3. try {
  4. //查询订单信息
  5. OrderVo orderVoResult = orderService.findOrderByTableId(tableId);
  6. //处理订单:可核算订单项目、购物车订单项目
  7. return handlerOrderVo(orderVoResult);
  8. }catch (Exception e){
  9. log.error("查询桌台订单信息异常:{}", ExceptionsUtil.getStackTraceAsString(e));
  10. throw new ProjectException(OrderEnum.SELECT_TABLE_ORDER_FAIL);
  11. }
  12. }
  13. /***
  14. * @description 处理当前订单中订单项
  15. * 从DB中查询当前订单可核算订单项
  16. * 从redis查询当前订单购物车订单项
  17. * @param orderVo 订单信息
  18. * @return
  19. */
  20. @Override
  21. public OrderVo handlerOrderVo(OrderVo orderVo)throws ProjectException {
  22. if (!EmptyUtil.isNullOrEmpty(orderVo)) {
  23. //1、查询MySQL:可核算订单项
  24. List<OrderItem> orderItemList = orderItemService.findOrderItemByOrderNo(orderVo.getOrderNo());
  25. List<OrderItemVo> orderItemVoStatisticsList = BeanConv.toBeanList(orderItemList, OrderItemVo.class);
  26. //2、可核算订单项总金额
  27. BigDecimal reducePriceStatistics = new BigDecimal("0");
  28. //2.1、处理空可核算订单项
  29. if (EmptyUtil.isNullOrEmpty(orderItemVoStatisticsList)) {
  30. orderItemVoStatisticsList = new ArrayList<>();
  31. }else {
  32. orderItemVoStatisticsList.forEach(n->{
  33. n.setAffixVo(affixFace.findAffixVoByBusinessId(n.getDishId()).get(0));
  34. });
  35. //2.2、计算可核算订单项总金额
  36. reducePriceStatistics = reducePriceHandler(orderItemVoStatisticsList);
  37. }
  38. //3、查询redis:购物车订单项
  39. String key = AppletCacheConstant.ORDERITEMVO_STATISTICS + orderVo.getOrderNo();
  40. RMapCache<Long, OrderItemVo> orderItemVoRMap = redissonClient.getMapCache(key);
  41. List<OrderItemVo> orderItemVoTemporaryList = (List<OrderItemVo>) orderItemVoRMap.readAllValues();
  42. //3.1、计算购物车订单项总金额
  43. BigDecimal reducePriceTemporary=reducePriceHandler(orderItemVoTemporaryList);
  44. //4、构建订单信息
  45. orderVo.setOrderItemVoStatisticsList(orderItemVoStatisticsList);
  46. orderVo.setReducePriceStatistics(reducePriceStatistics);
  47. orderVo.setOrderItemVoTemporaryList(orderItemVoTemporaryList);
  48. orderVo.setReducePriceTemporary(reducePriceTemporary);
  49. }
  50. return orderVo;
  51. }
  52. @Override
  53. public BigDecimal reducePriceHandler(List<OrderItemVo> orderItemVos )throws ProjectException{
  54. return orderItemVos.stream().map(orderItemVo -> {
  55. BigDecimal price = orderItemVo.getPrice();
  56. BigDecimal reducePrice = orderItemVo.getReducePrice();
  57. Long dishNum = orderItemVo.getDishNum();
  58. //如果有优惠价格以优惠价格计算
  59. if (EmptyUtil.isNullOrEmpty(reducePrice)) {
  60. return price.multiply(new BigDecimal(dishNum));
  61. } else {
  62. return reducePrice.multiply(new BigDecimal(dishNum));
  63. }
  64. }).reduce(BigDecimal.ZERO, BigDecimal::add);
  65. }
  • OrderItemServiceImpl
  1. @Override
  2. public List<OrderItem> findOrderItemByOrderNo(Long orderNo) {
  3. LambdaQueryWrapper<OrderItem> lambdaQueryWrapper = new LambdaQueryWrapper<>();
  4. lambdaQueryWrapper.eq(OrderItem::getProductOrderNo,orderNo);
  5. return list(lambdaQueryWrapper);
  6. }

课堂讨论

1、redis主从模式、哨兵模式、集群模式分别是什么,有什么特点?

  1. [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有那些?

  1. [https://blog.csdn.net/m0_53474063/article/details/113381122](https://blog.csdn.net/m0_53474063/article/details/113381122)

7、双写一致性如何解决?

  1. [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【☆☆☆】