# Notes **Repository Path**: studygo/Notes ## Basic Information - **Project Name**: Notes - **Description**: 开发相关的工作、学习笔记(源自出版书籍、网络博客、视频网课、实际工作等) - **Primary Language**: Java - **License**: LGPL-2.1 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-10-13 - **Last Updated**: 2022-03-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: 笔记 ## README # Notes 开发相关的工作、学习笔记(源自出版书籍、网络博客、视频网课、实际工作等) ## JAVA基础 ### 泛型 - 泛型将接口的概念进一步延伸,字面意思即“广泛的类型”。类、接口和方法代码可以应用于非常 广泛的类型,代码与它们能够操作的数据类型不再绑定到一起,同一套代码可以用于多种数据类型,这样,不仅可以复用代码, 降低耦合,而且可以提高代码的可读性和安全性。 - 泛型可以使用在接口、类、方法中,一般用来表示类型参数。泛型就是数据类型参数化,处理的数据类型不是固定的,而是可以 作为参数传入。 - 泛型的内部原理:java的泛型是靠“泛型擦除”来实现的。对于泛型类,java编译器会将泛型代码转换为普通的非泛型代码; 即将类型参数T擦除,替换为Object,插入必要的强制类型转换,在JVM实际运行程序时,并不知道泛型的实际类型参数。 - 泛型的作用:更好的安全性与更好的可读性。 程序设计的一个重要目标就是尽可能的将bug消灭在编译过程中,而不是运行时再发现。只是用Object的话,类型弄措时, 开发环境和编译器并不能帮我们发现问题;而使用泛型的话,若编写时弄错类型,开发环境或编译器会提示类型错误,这称之为“类型安全”。 因此泛型提高了程序的安全性;另外还省去了使用Object时繁琐的类型转换,明确了类型信息,使得代码的可读性也更好。 - 但要记住,泛型是靠“擦除”实现了,编译成.class字节码文件时泛型参数都是Object。 ### 内部类 - 内部类与包含它的外部类有比较密切的关系,而与其他类关系不大,定义在类内部,可以实现对外完全隐藏(private),可以有更好的封装性,代码实现上也往往更简洁 - **内部类只是java编译器的概念,对于jvm而言,它是不知道内部类这回事的,每个内部类最后都会被编译成一个独立的类,生产一个独立的字节码文件** , 即每个内部类其实都可以被替换成一个独立的类 - 内部类可以方便访问外部类的私有变量,可以声明private从而实现对外完全隐藏,从代码的可读性和可维护性上,说相关代码写在一起,写法也更为简洁。 - todo ## 日常搬砖 ### 单测 1. 个人心得: - 尤其对于业务繁琐,要扣细节,调用了大量第三方api,工具类的模块,要做好单测,精确到代码的最小运行单元--每一个方法. - 尤其要注意调用第三方api,尤其在处理金额(小数计算),Date(日期)的三方工具类方法的接口,这些方法引用自网络开源或者同事编写的, 源码逻辑看上去可能没什么问题,但由于小数/日期的特殊性,有一定埋坑的可能.单测这些工具类方法没有发现问题,引用了这些方法的接口service实现 也要重点测. - 保证小单元的准确性,才能避免模块测试时,不断出现形形色色的bug,排查起来远不如单测小单元时容易. 2. junit5的使用 - springboot集成junit ``` import com.ffw.springboot.MySpringBootApplication; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class)//以Spring启动类的方式运行测试类 @SpringBootTest(classes= MySpringBootApplication.class)//测试启动类入口,这里填写自己的 public class TestJunit { @AutoWired private UserMapper userMapper;//如有必要,可以直接注入mapper @Test//测试方法入口注解 public void testJunit(){ System.out.println("我是单元测试"); //userMapper.queryAllUser();//如有必要,可直接调用mapper方法测试sql } } ``` --- - @Test注解在方法上标记方法为测试方法,以便构建工具和IDE能够识别执行它们.Junit5不再需要手动将测试类与测试方法为public,包可见的访问级别足矣.eg: ``` @Test void testDemo() { assertEquals(2, 1 + 1); } ``` - 初始化与销毁: 需要执行一些代码来在测试逻辑执行前后完成一些初始化或销毁的操作.Junit5中有4个注解可能会用于如此工作: + @BeforeAll 只执行一次,执行时机是在所有测试和@BeforeEach注解方法之前 + @BeforeEach 在每次测试执行之前执行 + @AfterEach 在每个测试执行之后执行 + @AfterAll 只执行一次,执行时机是在所有测试和@AfterEach注解方法之后 - 断言 + assertEquals(x,y) ; assertEquals(x,y,z) + assertTrue(true, () -> {...} ); + assertFalse(x) + assertNotNull(Object obj) 检查对象不为空 + assertNull(Object obj) 检查对象为空 + assertArrayEquals(expectrArr,resultArr) 检查两个数组是否相等 + assertAll + ... - junit测试方法超时与否 eg: ``` @Test @Timeout(value = 100, unit = TimeUnit.MILLISECONDS) void testTimeOut(){ ... } ``` - 异常测试 + Junit 用代码处理提供了一个追踪异常的选项。你可以测试代码是否它抛出了想要得到的异常。expected 参数和 @Test 注释一起使用 ``` @Test(expected = NullPointerException) ``` - 参数测试 略... - 其他注解 + @After @Before @Ignore(忽略不需要执行的测试) 3. HTTP 304响应状态码详解 - 304状态码是对客户端缓存情况下服务端的一种响应. - 客户端在请求一个文件时,发现自己缓存的文件有Last Modefied, 那么在请求中会包含If Modified Since,这个时间就是缓存文件的Last Modified. 因此如果请求中包含If Modified Since,就说明已经有缓存在客户端.服务端只要判断这个时间和当前请求的文件的修改时间,就可以确定是返回304还是200. 因此,对于动态页面做缓存加速,首先要在Response的Http Header中增加Last Modified定义,其次根据Request中的If Modified Since和被请求的内容 更新时间来返回200或304.这样返回304时就避免了数据库查询,并且没用返回文件而只是一个Http Header,从而大大降低了带宽的消耗. --- ## 常见场景解决方案 ### 用户下订单30分钟未支付,则自动取消的实现 > 容易想到的方案: 数据库轮询; 但除了小型项目否则不会采用;因为对服务器内存消耗过大,对数据库资源占用过多,并且存在延迟--最大的延迟时间就是轮询的间隔时间 - jdk的延迟队列 - 优势: 效率高,任务触发时间延迟低. - 缺点: 1. 服务器一旦宕机/重启,线程内存中的订单数据便全部消失 2. 不适合集群扩展 3. 由于内存条件限制的原因,一旦下单未付款的数太多,便会出现OOM异常 - 固综合来说,这种方法在实践中不现实,不予采纳! ``` @Data public class OrderDelay implements Delayed { //订单id private String orderId; //超时时刻,单位纳秒 private long timeout; public OrderDelay(String orderId, long timeout) { this.orderId = orderId; this.timeout = TimeUnit.NANOSECONDS.convert(timeout,TimeUnit.SECONDS) + System.nanoTime(); } //返回值小于等于0时即延时时间到 @Override public long getDelay(TimeUnit unit) { return unit.convert(timeout- System.nanoTime(),TimeUnit.NANOSECONDS); } //定义延时队列的元素排序规则 @Override public int compareTo(Delayed o) { if(o==this) return 0; OrderDelay orderDelay = (OrderDelay) o; long d = this.getDelay(TimeUnit.NANOSECONDS) - orderDelay.getDelay(TimeUnit.NANOSECONDS); return d==0 ? 0 : (d > 0 ? 1 : -1) ; } public void print(){ System.out.println("删除订单............................"+this.orderId); } } public class OrderDelayDemo { public static void main(String[] args) throws InterruptedException { Set orderIds = new LinkedHashSet(){{add("00000001");add("00000002");add("00000003");add("00000004");add("00000005");}}; DelayQueue queue = new DelayQueue(); int second = 0; //延迟的秒数 for (String orderId : orderIds) { queue.put( new OrderDelay(orderId,++second) ); } long start = System.currentTimeMillis(); while (true){ OrderDelay take = queue.take(); long end = System.currentTimeMillis(); System.out.println( (end-start) + "毫秒后删除订单"+take.getOrderId() ); } } } ``` - 时间轮算法 - 优点: 效率高,任务触发时间延迟时间比delayQueue低,代码复杂度比delayQueue低 - 缺点: 1. 一旦服务器宕机/重启,数据便全部丢失 2. 不适用集群扩展 3. OOM异常风险 ``` @Data @AllArgsConstructor public class OrderTimerTask implements TimerTask { private long orderId; private long timeout; //延时时间 单位秒 private TimeUnit unit; public void run(Timeout timeout) throws Exception { System.out.println("删除xxx订单....................."+orderId); } } /** * netty框架提供的时间轮工具 * io.netty.util.HashedWheelTimer 来实现达到指定延时时间时开异步线程来删除未支付的订单 */ public class HashWheelTimerDemo { private static Timer timer = new HashedWheelTimer(); public static void main(String[] argv) throws InterruptedException { OrderTimerTask orderTimerTask = new OrderTimerTask(100000L, 8,TimeUnit.SECONDS); //异步线程,指定时间后执行其中run方法 timer.newTimeout(orderTimerTask, orderTimerTask.getTimeout(), orderTimerTask.getUnit()); TimeUtil.stopwatch(orderTimerTask.getTimeout()+1); } } ``` - redis缓存 - 思路一: 利用redis得zset,zset是一个有序集合,每一个元素(member)都关联了一个score,通过score排序来取集合中的值 ![redis-zset结构解决延时执行任务队列的需求](https://gitee.com/studygo/Notes/raw/main/imgs/redis-zset-demo01.png) ``` public class RedisDemo { private static Jedis jedis = JedisUtil.getJedis(); public static void main(String[] args) { RedisDemo redisDemo = new RedisDemo(); redisDemo.produceDelayMessage(); redisDemo.consumerDelayMessage(); } public void produceDelayMessage(){ for(int i=1;i<=5;i++){ //延迟3秒 Calendar cal1 = Calendar.getInstance(); cal1.add(Calendar.SECOND, 10 + i * 5); int secondslater = (int) (cal1.getTimeInMillis() / 1000 ); jedis.zadd("OrderId",secondslater,"1000000"+i); //添加到期时间戳(score),与订单号(member) System.out.println(System.currentTimeMillis()+"ms:redis生成了一个订单任务:订单ID为"+"1000000"+i); } } //消费者,取订单 public void consumerDelayMessage(){ while(true){ //zset是按score大小升序排序的 Set items = jedis.zrangeWithScores("OrderId", 0, 1); //取排序的第一个元素 if(CollectionUtils.isEmpty(items)){ System.out.println("当前没有等待的任务"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } continue; } int score = (int) ((Tuple)items.toArray()[0]).getScore(); Calendar cal = Calendar.getInstance(); int nowSecond = (int) (cal.getTimeInMillis() / 1000); if(nowSecond >= score){ //当前时间大于过期时间 String orderId = ((Tuple)items.toArray()[0]).getElement(); // jedis.zrem("OrderId", orderId); Long zrem = jedis.zrem("OrderId", orderId); //防止多线程时消费了同样的订单 if(zrem!=null && zrem.compareTo(0L)>0 ){ //处理订单的逻辑操作 System.out.println(System.currentTimeMillis() +"ms:redis消费了一个任务:消费的订单OrderId为"+orderId); } } } } } ``` - 思路二: 使用redis KEY的"键空间机制" (Keys paceNotifications); 该机制可在key失效后,提供一个回调,即redis在有键失效后会给客户端发送一条消息(需要redis版本2.8+) > 由于redis的pub/sub(发布订阅) 机制存在硬伤,官网内容如下: >> Because Redis Pub/Sub is fire and forget currently there is no way to use this feature if your application demands reliable notification of events, that is, if your Pub/Sub client disconnects, and reconnects later, all the events delivered during the time the client was disconnected are lost. Redis的发布/订阅目前是即发即弃(fire and forget)模式的,因此无法实现事件的可靠通知。也就是说,如果发布/订阅的客户端断链之后又重连,则在客户端断链期间的所有事件都丢失了。 因此,方案二不是太推荐。当然,如果你对可靠性要求不高,可以使用。 - 固第二种方式实际中不用使用,仅了解即可. - 使用redis处理的优点: 1. 首先数据都存储在redis中,即使发送程序(生产)和任务处理程序(消费)挂了,重启后依然有重新处理数据的可能性. (只会有这种情况丢失数据,即生产方发送了数据,从延迟队列中移除后,在消费程序处理完之前 挂了,这种情况下重启后就丢失了要处理的数据,固可以对生产方队列中的数据做个备份,记录消费状态,设置几天的过期时间,给万一出现上述情况时留手动处理的缓冲时间) 2. redis易扩展,集群 3. 时间准确度可以控制得比较高 - 缺点: 1. redis运维成本(强行缺点,现在什么项目不用redis???) - 消息中间件 - todo --- ### springboot整合aop应用与系统日志 对所有web请求(controller层)作切面来记录日志 1. 引入spring-aop依赖 2. 用注解(@Component, @Aspect)设置目标切面类 3. 切面类中通过 @Pointcut定义的切入点,通过 @Before实现切入点的前置通知,@After作后置最终通知 通过 @AfterReturning记录请求返回的对象, 通过后置异常通知@AfterThrowing在(连接点的)方法抛出异常退出时执行的通知, 环绕通知@Around:包围一个连接点的通知,如方法调用等。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为,它也会选择是否继续执行连接点或者直接返回它自己的返回值或抛出异常来结束执行。 4. 若切面中需要使用到目标对象的某个参数,如何使切面能得到目标对象的参数呢?可以使用args来绑定。如果在一个args表达式中应该使用类型名字的地方使用一个参数名字, 那么当通知执行的时候对象的参数值将会被传递进来。 ``` @Before("execution(* findById*(..)) &&" + "args(id,..)") public void beforeMethod(Long id){ System.out.println ("切面before执行了。。。。id==" + id); } ``` 5. 所有通知方法都可以将第一个参数定义为org.aspectj.lang.JoinPoint类型(环绕通知需要定义第一个参数为ProceedingJoinPoint类型,它是 JoinPoint 的一个子类)。 JoinPoint接口提供了一系列有用的方法,比如 getArgs()(返回方法参数)、 getThis()(返回代理对象)、getTarget()(返回目标)、getSignature()(返回正在被通知的方法相关信息)和 toString()(打印出正在被通知的方法的有用信息)。 ### Mq之rocketMq > 核心概念 >> 消息: 生产者向Topic发送并最终传送给消费者的数据和(可选)属性集合 >> 消息属性:生产者可为消息定义的属性,包括Message Key和Tag >> Topic:消息主题,一级消息类型,通过Topic对消息进行分类.生产者向其发送消息. >>> Topic与Tag都是业务上用来归类的标识,区分在于Topic是一级分类,而Tag可以理解为是二级分类.且可以利用Topic和Tag来实现消息过滤. >> 生产者:负责生产并发送消息至Topic >> 消费者:负责从Topic接收并消费消息;可分为两类: >>> Push Consumer: 消息由消息队列RocketMQ版推送至Consumer. >>> Pull Consumer: 该类Consumer主动从消息队列RocketMQ版拉取消息 (目前仅TCP Java SDK支持该类Consumer) >> Group:一类生产者或消费者,这类生产者或消费者通常生产或消费同一类消息,且消息发布或订阅的逻辑一致. >> 略 (更多的参见 [阿里云官方文档](https://help.aliyun.com/document_detail/29533.htm?spm=a2c4g.11186623.0.0.7fb56401P1HQvb#concept2655) ![rocketmq的Topic与Tag关系](https://gitee.com/studygo/Notes/raw/main/imgs/rocketmq-topic-tag.png) 关于Topic与Tag的应用;什么时候该用Topic,什么时候改用Tag? - 消息类型是否一致: 普通消息/事务消息/延时消息/顺序消息,不同消息类型使用不同Topic,无法通过Tag进行区分 - 业务是否相关联: 没有直接关联的消息,如淘宝交易消息,京东物流消息使用不同的Topic区分;而同样是天猫交易消息,电器类订单,女装类订单,化妆品类订单的消息可用Tag进行区分. - 消息优先级是否一致: 如同样是物流消息,盒马,天猫超市,淘宝的物流速度不一样,不同优先级的消息用不同的Topic进行区分 - 消息量级是否相当:有些业务消息虽然量小但实时性要求高,如果跟某些万亿量级消息使用同一个Topic,则有可能因为过长的等待时间而"饿死",此时需要将不同量级的消息进行拆分,使用不同的Topic. ***总的来说针对消息分类,可以选择创建多个Topic,或者在同一个Topic下创建多个Tag.但通常情况下,不同的Topic间消息没有必然的联系,而Taf则用来区分同一个Topic下相互关联的消息,例如全集和子集的关系,流程先后的关系*** > RocketMq发送普通消息有三种方式: 1. 同步发送(reliable synchronous), 2.异步发送(reliable asynchronous), 3.直接发送(one-way transmission) >> (reliable synchronous)可靠的同步传输广泛应用于重要通知消息、短信通知、短信营销系统等场景. >> (reliable asynchronous)异步传输通常用于响应时间敏感的业务场景. >> (one-way transmission)单向传输适用于对可靠性要求普通(较低)的场合,如日志收集等. --- ![普通消息同步发送示意图](https://gitee.com/studygo/Notes/raw/main/imgs/同步发送示意图.png) --- ![普通消息异步发送示意图](https://gitee.com/studygo/Notes/raw/main/imgs/异步发送示意图.png) --- ![普通消息单向发送示意图](https://gitee.com/studygo/Notes/raw/main/imgs/单向发送示意图.png) --- ![rocketmq的普通消息三种发送方式特点对比](https://gitee.com/studygo/Notes/raw/main/imgs/rocketmq的三种发送方式特点对比.png) --- 实际应用中对发送方式的选择如下: - 当发送的消息不重要时,采用one-way方式,以提高吞吐量 - 发送的消息很重要且对响应时间不敏感时,采用同步方式 - 发送的消息很重要且对响应时间敏感时,采用异步方式 rocketMq消息类型有: - 普通消息 - 普通消息有三种发送方式: 同步发送; 异步发送; 单向发送; - 事务消息 - 延时消息 - 顺序消息 ##### 多线程发送消息 消费者和生产者客户端对象是线程安全的,可以在多个线程之间共享使用.可以在服务器(或多台服务器)部署多个生产者和消费者实例, 也可以在同一个生产者或消费者实例里采用多线程发送或接收消息,从而提高消息发送或接收TPS ``` import com.aliyun.openservices.ons.api.Message; import com.aliyun.openservices.ons.api.Producer; import com.aliyun.openservices.ons.api.ONSFactory; import com.aliyun.openservices.ons.api.PropertyKeyConst; import com.aliyun.openservices.ons.api.SendResult; import java.util.Properties; public class SharedProducer { public static void main(String[] args) { // producer实例配置初始化。 Properties properties = new Properties(); // 您在消息队列RocketMQ版控制台创建的Group ID。 properties.put(PropertyKeyConst.GROUP_ID,"XXX"); // AccessKey ID阿里云身份验证,在阿里云服务器管理控制台创建。 properties.put(PropertyKeyConst.AccessKey,"XXX"); // AccessKey Secret阿里云身份验证,在阿里云服务器管理控制台创建。 properties.put(PropertyKeyConst.SecretKey,"XXX"); // 设置发送超时时间,单位:毫秒。 properties.setProperty(PropertyKeyConst.SendMsgTimeoutMillis,"3000"); // 设置TCP接入域名,进入消息队列RocketMQ版控制台实例详情页面的接入点区域查看。 properties.put(PropertyKeyConst.NAMESRV_ADDR, "XXX"); final Producer producer = ONSFactory.createProducer(properties); // 在发送消息前,必须调用start方法来启动Producer,只需调用一次即可。 producer.start(); //创建的Producer和Consumer对象为线程安全的,可以在多线程间进行共享,避免每个线程创建一个实例。 //在thread和anotherThread中共享Producer对象,并发地发送消息至消息队列RocketMQ版。 Thread thread = new Thread(new Runnable() { @Override public void run() { try { Message msg = new Message( // 普通消息所属的Topic,切勿使用普通消息的Topic来收发其他类型的消息。 "TopicTestMQ", // Message Tag可理解为Gmail中的标签,对消息进行再归类,方便Consumer指定过滤条件在消息队列RocketMQ版的服务器过滤。 "TagA", // Message Body可以是任何二进制形式的数据,消息队列RocketMQ版不做任何干预。 // 需要Producer与Consumer协商好一致的序列化和反序列化方式。 "Hello MQ".getBytes()); SendResult sendResult = producer.send(msg); // 同步发送消息,只要不抛异常就是成功。 if (sendResult != null) { System.out.println(new Date() + " Send mq message success. Topic is:" + MqConfig.TOPIC + " msgId is: " + sendResult.getMessageId()); } } catch (Exception e) { // 消息发送失败,需要进行重试处理,可重新发送这条消息或持久化这条数据进行补偿处理。 System.out.println(new Date() + " Send mq message failed. Topic is:" + MqConfig.TOPIC); e.printStackTrace(); } } }); thread.start(); Thread anotherThread = new Thread(new Runnable() { @Override public void run() { try { Message msg = new Message("TopicTestMQ", "TagA", "Hello MQ".getBytes()); SendResult sendResult = producer.send(msg); // 同步发送消息,只要不抛异常就是成功。 if (sendResult != null) { System.out.println(new Date() + " Send mq message success. Topic is:" + MqConfig.TOPIC + " msgId is: " + sendResult.getMessageId()); } } catch (Exception e) { // 消息发送失败,需要进行重试处理,可重新发送这条消息或持久化这条数据进行补偿处理。 System.out.println(new Date() + " Send mq message failed. Topic is:" + MqConfig.TOPIC); e.printStackTrace(); } } }); anotherThread.start(); //(可选)Producer实例若不再使用时,可将Producer关闭,进行资源释放。 // producer.shutdown(); } } ``` ##### 收发顺序消息 顺序消息分为两类: - 全局顺序: 对于指定的一个Topic,所有消息按照严格的先入先出FIFO(First in First Out)的顺序进行发布和消费 - 分区顺序: 对于指定的一个Topic,所有消息根据Sharding Key进行区块分区.同一个分区内的消息按照严格的FIFO顺序进行发布和消费(不同分区间之间的消息顺序不做要求). Sharding Key是顺序消息中用来区分不同分区的关键字段,和普通消息的Key是完全不同的概念. --- 顺序消息的补充说明: - 分区顺序消息适用于性能要求高,以Sharding Key作为分区字段,在同一个区块中严格地按照先进先出(FIFO)原则进行消息发布和消费的场景 - 全局顺序消息适用于性能要求不高,所有消息严格按照FIFO原则来发布和消费的场景 - 全局顺序消息实际是一种特殊的分区顺序消息,即Topic中只有一个分区,因此全局顺序和分区顺序的实现原理相同.因为分区顺序消息有多个分区,所以分区顺序消息比全局顺序消息的并发度和性能更高. ### 幂等设计 - 幂等用数学公式表示就是f(x)=f(f(x)),在计算机科学中,幂等表示一次和多次请求某一个资源应该具有相同的副作用,或者说,多次请求所产生的影响与一次请求执行的影响效果相同。 - 实际场景中常见需要的考虑幂等的操作 - 转账 - MQ 消费者读取消息时,有可能会读到重复的消息(重复消费) - 提交form表单时,如果快速点击提交按钮,可能产生了两条一样的数据(前端重复消费) - 当前的互联网系统几乎都是解耦隔离后,会存在各个不同系统的相互远程调用。远程调用服务会有三个状态:成功,失败,超时。前两者是明确的状态,而超时是未知状态。 转账超时时,如果下游转账系统做好幂等控制,我们可以发起重试,那既可以保证转账正常进行,又可以保证不会多转一笔。 - 接口超时时,有两种方案处理: - 上游系统提供一个对应的查询接口。接口超时时,先查询对应的记录,结果是成功就走成功流程,失败就按失败处理(发起重试)。 - 下游接口支持幂等,上游系统如果调用超时,直接重试即可。(优先选择此方案) - 如何设计幂等? 幂等意味着请求的唯一性,不管是哪个方案去设计幂等,都需要一个全局唯一的ID,去标记这个请求是独一无二的。 - 实现幂等的8种方案 1. select查询主键/唯一索引 + insert 2. insert主键/唯一索引 (冲突就会插入失败) 3. 状态机(状态变更图) : 对于有状态的业务表,比如提现记录表,有-1失败,0体现中,1提现完成,记录更新时状态也会更新,update语句条件中带上变更前的状态,如果状态已经是当前期望的那就不会update成功 4. 抽取防重表 5. token令牌(token是全局唯一的,客户端每次请求携带一个全局唯一的token去redis里校验是否存在) 6. 悲观锁 ``` //伪码如下 begin; select * from order where order_id = '666' for update; //查询订单,判断状态,锁住这一行记录,别的线程请求过来只能等待 if(status != 处理中){ //非处理中状态,直接返回 return; } update order set status = '完成' where order_id = '666'; #更新完成 commit; ``` 7. 乐观锁(version版本号): 建议版本号自增,因为乐观锁存在ABA问题,如果version一直自增就不会存在 8. 分布式锁 ### 数据库设计 #### 设计三范式 1. 第一范式是最基本的;数据库表中所有的字段值都需是不可分的原子值。
需要根据系统的实际需求来遵循。如某些数据库系统中需要用到“地址”这个属性,本来直接将“地址”属性设计成一个数据库表的字段就行, 但如果系统经常会访”地址“属性中的“城市”部分,那么就非要将“地址”这个属性重新拆分成省份、城市、详细地址等多个部分进行存储, 这样在对地址中某一部分操作的时候将非常方便。 2. 第二范式主要针对联合主键而言,确保数据库中的每一列都和联合主键相关,而不能只与联合主键的某一部分相关;
也就是说一张表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张表中;
比如要设计一个订单信息表,因为订单中可能会有多种商品,所以要将订单编号和商品编号作为数据库表的联合主键; 这样在该表中商品名称,单位,价格等信息不与该表的联合主键(订单id+商品id)相关,而仅仅与商品编号相关; 所以就要把商品的字段都拆分出来到商品表中;这样能减少数据冗余。 3. 第三范式要确保数据库表中的每一列数据都和主键直接相关,而不能间接相关;