事务定义
在数据库管理系统中,事务是单个逻辑或工作单元,有时由多个操作组成,在数据库中以一致模式完成的逻辑处理称为事务。一个例子是从一个银行账户转账到另一个账户:完整的交易需要减去从一个账户转账的金额,然后将相同的金额添加到另一个账户。
事务特性
原子性(Atomicty)
一致性(Consistency)
隔离性(Isolation)
持久性(Durability)
原子性( atomicty)
事务中的全部操作在数据库中是不可分割的,要么全部完成,要么全部不执行。
一致性(consistency)
事务的执行不能破坏数据库数据的完整性和一致性。一致性指数据满足所有数据库的条件,比如字段约束、外键约束、触发器等,事务从一致性开始,以一致性结束。
隔离性( isolation)
事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务是透明的。
持久性(durability)
对于提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。
注: DBMS一般采用日志来保证事务的原子性、一致性和持久性。
事务隔离级别
并发事务带来的问题
脏读Dirty read
当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
丢失修改Lost to modif
指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据,这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1并提交,事务2也修改A=A-1并提交,最终结果A=19,事务1的修改被丢失。
不可重复读Unrepeatable read
指在一个事务内多次读同一数据,在这个事务还没有结束时,另一个事务也访问该数据。在第一个事务中的两次读数据之间,由于第二个事务的修改并提交导致第一个事务两次读取的数据可能不一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
幻读Phantom read
幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据并提交。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
不可重复读的重点是数据修改场景,幻读的重点在于新增或者删除场景。
SQL92标准定义了4种隔离级别的事务
读未提交Read uncommitted
事务可以读取未提交的数据。事务中的修改,即使没有提交,对其他事务也都是可见的(解决 脏读 不可重复读 幻读)
读已提交Read committed
一个事务开始后,只能看到已经提交的事务所做的修改(解决 不可重复读 幻读)
可重复读Repeatable read
在同一个事务中多次读取同样的记录的结果是一致的。它解决了脏读和不可重复读问题,但还是无法解决幻读问题
可串行化Serializable
通过强制事务串行,避免了前面说的幻读问题,实际应用中很少
大多数数据库系统如oracle的默认隔离级别都是 Read committed,mysql默认为可重复读,InnoDB 和 XtraDB 存储引擎通过多版并发控制(MVCC,Multivesion Concurrency Control)解决了幻读问题,Repeatable read 是 Mysql 默认的事务隔离级别,其中 InnoDB主 要通过使用 MVVC 获得高并发,使用一种被称为 next-key-locking 的策略来避免幻读。
事务模型
事务提交模型
显式事务: 又称自定义事务,是指用显式的方式定义其开始和结束的事务,当使用start transaction和 commit语句时表示发生显式事务。
隐式事务: 隐式事务是指每一条数据操作语句都自动地成为一个事务,事务的开始是隐式的,事务的结束有明确的标记。即当用户进行数据操作时,系统自动开启一个事务,事务的结束则需手动调用 commit或 rollback语句来结束当前事务,在当前事务结束后又自动开启一个新事务。
自动事务: 自动事务是指能够自动开启事务并且能够自动结束事务。在事务执行过程中,如果没有出现异常,事务则自动提交;当执行过程产生错误时,则事务自动回滚;一条SQL语句一个事务。
事务编程模型
本地事务模型: 事务由本地资源管理器来管理。简单理解就是直接使用JDBC的事务API。
connection.setAutoCommit(false);// 自动提交关闭//XXXX数据库的增删改查操作connection.commit(); //提交事务
编程式事务模型: 事务通过JTA以及底层的JTS实现来管理,对于开发人员而言,管理的是“事务”,而非“连接”。简单理解就是使用事务的API写代码控制事务。
示例一、JTA的API编程
UserTransaction txn = sessionCtx.getUserTransaction();txn.begin();txn.commit();
示例二、Spring的事务模版
transactionTemplate.execute( new TransactionCallback() { @Override public Object doInTransaction(TransactionStatus status) { // 事务相关处理 return null; } }); 声明式事务: 事务由容器进行管理,对于开发人员而言,几乎不管理事务。简单理解就是加个事务注解或做个AOP切面。 @Transactional(rollbackFor = Exception.class)public void updateStatus(String applyNo){ cashierApplyMapper.updateStatus(applyNo, CANCEL_STATUS, CANCEL_STATUS_DESC);} 本地事务模型 自主可控 侵入性较大,开发人员时刻关注事务边界,写大量commit代码,不支持XA 编程式事务模型 模型高了一个层级,自主可控性强 需要写代码,不优雅 声明式事务模型 简单易用 事务边界必须是方法级别,依赖AOP的机制 SQL的全称: Structured Query Language。中文翻译:结构化查询语言 以Spring+Mybatis+JDBC+Mysql为例,常见的事务类请求的调用链路如下图。请求调用应用服务,应用服务中开启事务并进行业务操作,操作过程中调用Mybatis进行数据库类操作,Mybatis通过JDBC驱动与底层数据库交互。 请求方->请求接受入口->Spring事务->业务操作->Mybatis->JDBC->数据库MySql 因此接下来先按Mysql、JDBC、Mybatis、Spring来介绍各层的事务相关知识;最后进行全链路的调用分析。 Mysql事务相关 Mysql逻辑架构 架构图如下(InnoDB存储引擎): 客户端连接线程处理(连接处理,授权认证,安全)查询缓存(分析器->优化器->执行器)查询,解析,分析,优化,缓存InnoDB存储引擎(Buffer Pool缓冲池)(Redo Log Buffer),undo日志文件,磁盘文件,redo日志文件,binlog文件(存储和读取数据) MySQL事务是由存储引擎实现的,MySQL支持事务的存储引擎有InnoDB、NDB Cluster等,其中InnoDB的使用最为广泛,其他存储引擎如MyIsam、Memory等不支持事务。 Mysql的事务保证 Mysql的4个特性中有3个与 WAL(Write-Ahead Logging,先写日志,再写磁盘)有关系,需要通过 Redo、Undo 日志来保证等,而一致性需要通过DBMS的功能逻辑及原子性、隔离性、持久性共同来保证。 日志机制(Redo/Undo):原子性,隔离性,持久性 DBMS功能逻辑:一致性 利用Hutool+自定义注解实现数据脱敏 首先我们要先根据以上Hutool中提供的脱敏类型来编写我们自己的类型 或者 直接使用DesensitizedUtil中的DesensitizedType public enum DataMaskingType { /** * 用户ID */ USER_ID, /** * 中文名 */ CHINESE_NAME, /** * 身份证号 */ ID_CARD, /** * 座机 */ FIXED_PHONE, /** * 手机号 */ MOBILE_PHONE, /** * 地址 */ ADDRESS, /** * 邮箱 */ EMAIL, /** * 密码 */ PASSWORD, /** * 中国大陆车牌号 */ CAR_LICENSE, /** * 银行卡号 */ BANK_CARD, /** * IPv4地址 */ IPV4, /** * IPv6地址 */ IPV6, /** * 自定义类型 */ CUSTOM; } 编写自定义注解 @Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@JacksonAnnotationsInside@JsonSerialize(using = DataMaskingSerialize.class)public @interface DataMasking { /** * 数据脱敏类型 */ DataMaskingType type() default DataMaskingType.CUSTOM; /** * 脱敏开始位置(包含) */ int start() default 0; /** * 脱敏结束位置(不包含) */ int end() default 0; } 需要注意的是:当DataMaskingType为 CUSTOM 时,才需要填写 start 和 end ,且这两个参数才会生效,且 start 中是包含当前下标的字符的,而 end 不包含当前下标的字符。 编写自定义序列化类 @AllArgsConstructor@NoArgsConstructorpublic class DataMaskingSerialize extends JsonSerializer implements ContextualSerializer { private DataMaskingType type; private Integer start; private Integer end; @Override public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { String value = (String) o; switch (type) { //userId case USER_ID: jsonGenerator.writeString(String.valueOf(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.USER_ID))); break; //中文名 case CHINESE_NAME: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.CHINESE_NAME)); break; //身份证号 case ID_CARD: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.ID_CARD)); break; //座机 case FIXED_PHONE: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.FIXED_PHONE)); break; //手机号 case MOBILE_PHONE: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.MOBILE_PHONE)); break; //地址 case ADDRESS: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.ADDRESS)); break; //邮箱 case EMAIL: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.EMAIL)); break; case BANK_CARD: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.BANK_CARD)); break; //密码 case PASSWORD: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.PASSWORD)); break; //中国大陆车牌号 case CAR_LICENSE: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.CAR_LICENSE)); break; case IPV4: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.IPV4)); break; case IPV6: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.IPV6)); break; //自定义 case CUSTOM: jsonGenerator.writeString(CharSequenceUtil.hide(value, start, end)); break; default: break; } } @Override public JsonSerializer createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException { if (Objects.nonNull(beanProperty)) { //判断是否为string类型 if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) { DataMasking anno = beanProperty.getAnnotation(DataMasking.class); if (Objects.isNull(anno)) { anno = beanProperty.getContextAnnotation(DataMasking.class); } if (Objects.nonNull(anno)) { return new DataMaskingSerialize(anno.type(), anno.start(), anno.end()); } } return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty); } return serializerProvider.findNullValueSerializer(null); }} 继承于 JsonSerializer 并实现了 ContextualSerializer 中的方法,并对我们自定义注解声明的字段进行拦截和脱敏加密操作 @Data@Builder@ToString@AllArgsConstructor@NoArgsConstructorpublic class TestEntity { @DataMasking(type = DataMaskingType.USER_ID) private Integer userId; @DataMasking(type = DataMaskingType.CHINESE_NAME) private String userName; @DataMasking(type = DataMaskingType.ADDRESS) private String address; @DataMasking(type = DataMaskingType.ID_CARD) private String idCard; @DataMasking(type = DataMaskingType.FIXED_PHONE) private String fixedPhone; @DataMasking(type = DataMaskingType.MOBILE_PHONE) private String mobilePhone; @DataMasking(type = DataMaskingType.EMAIL) private String email; @DataMasking(type = DataMaskingType.PASSWORD) private String password; @DataMasking(type = DataMaskingType.CAR_LICENSE) private String carLicense; @DataMasking(type = DataMaskingType.BANK_CARD) private String bankCard; @DataMasking(type = DataMaskingType.IPV4) private String ipv4; @DataMasking(type = DataMaskingType.IPV6) private String ipv6; @DataMasking(type = DataMaskingType.CUSTOM,start = 3,end = 9) private String custom; /** * 不进行数据脱敏的字段 */ private String noMask; } @RestControllerpublic class TestController { @GetMapping("/test") public TestEntity test() { return TestEntity.builder() .userId(1234567890) .userName("张三") .password("12") .address("河南省郑xxx区") .email("xxxx@xx.com") .fixedPhone("08xxxxx792") .mobilePhone("138xxxx888") .carLicense("豫P3xxx3") .bankCard("167xxxxx740") .idCard("4127xxxx6677") .ipv4("192xxxx.236") .ipv6("abcd:12xxxxx567:089:0:0000") .custom("2xxxx58794") .noMask("我是不需要数据脱敏的字段") .build(); } } 使用了Hutool的DesensitizedUtil中的 desensitized 方法来实现数据脱敏,在 CUSTOM 类型的脱敏字段中,start 和 end 两个属性是必填的,且 start 包含当前下标,而 end 不包含当前下标。 浅谈Mysql读写分离的坑以及应对的方案 一、主从架构 为什么我们要进行读写分离?个人觉得还是业务发展到一定的规模,驱动技术架构的改革,读写分离可以减轻单台服务器的压力,将读请求和写请求分流到不同的服务器,分摊单台服务的负载,提高可用性,提高读请求的性能。 一个基础的Mysql的主从架构,1主1备3从。这种架构是客户端主动做的负载均衡,数据库的连接信息一般是放到客户端的连接层,也就是说由客户端来选择数据库进行读写 一个带proxy的主从架构,客户端只和proxy进行连接,由proxy根据请求类型和上下文决定请求的分发路由。 两种架构方案各有什么特点: 1.客户端直连架构,由于少了一层proxy转发,所以查询性能会比较好点儿,架构简单,遇到问题好排查。但是这种架构,由于要了解后端部署细节,出现主备切换,库迁移的时候客户端都会感知到,并且需要调整库连接信息 2.带proxy的架构,对客户端比较友好,客户端不需要了解后端部署细节,连接维护,后端信息维护都由proxy来完成。这样的架构对后端运维团队要求比较高,而且proxy本身也要求高可用,所以整体架构相对来说比较复杂 但是不论使用哪种架构,由于主从之间存在延迟,当一个事务更新完成后马上发起读请求,如果选择读从库的话,很有可能读到这个事务更新之前的状态,我们把这种读请求叫做过期读。出现主从延迟的情况有多种,有兴趣的同学可以自己了解一下,虽然出现主从延迟我们同样也有应对策略,但是不能100%避免,这些不是我们本次讨论的范围,我们主要讨论一下如果出现主从延迟,刚好我们的读走的都是从库,我们应该怎么应对? 首先我把应对的策略总结一下: 强制走主库 sleep方案 判断主从无延迟 等主库位点 等GTID方案 ThreadLocal:线程中的全局变量 最近接了一个新需求,业务场景上需要在原有基础上新增2个字段,接口新增参数意味着很多类和方法的逻辑都需要改变,需要先判断是否属于该业务场景,再做对应的逻辑。原本的打算是在入口处新增变量,在操作数据的时候进行逻辑判断将变量进行存储或查询。 如果全链路都变更入参和结构,很明显代码上很不优雅,后续如果还要增加业务场景,又需要再改一遍。如果有一个方法可以传递全局变量,而且仅限于当前线程就好了。 到此,会想到有两种解决方案:之前用的比较少的ThreadLocal或者使用redis缓存。考虑到新增字段都是些增删改查的操作,没有必要存到redis中,故使用ThreadLocal。 一、ThreadLocal定义 以微服务架构为例,服务提供方在收到调用方的请求后,会把这个请求分配给一个线程进行处理。一般来说,一个请求会一直由同一个线程处理,中间不会切换线程,所以如果有一个线程中共享的变量,可以当全局变量使用。 ThreadLocal实现的就是一个线程中的全局变量,与真正的全局变量的区别在于ThreadLocal的变量是每个线程中的全局变量,也就是说不同线程访问到的值是不一样的。其填充的变量属于当前线程,该变量对于其他线程是隔离的。 由定义可以发现,ThreadLocal有两个特性:每个Thread的变量只能由当前Thread使用;由于其他线程不可访问,则不存在多线程间共享的问题。 二、修饰 ThreadLocal提供了线程本地的实例,它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。 ThreadLocal变量通常被private static修饰,这样的好处是当一个线程结束时,它所使用的ThreadLocal实例副本都可被回收,避免重复创建。坏处就是这样做可能正好导致内存泄漏。 三、底层实现 ThreadLocal最朴素的内部实现是Map,这是一个HashMap,又称为ThreadLocalMap。但Java源码并不是Map的实现。这是因为如果多个线程访问同一个map,这个map需要是线程安全的,构造比较麻烦。Java采用了更简单粗暴的做法:每个线程都有自己的ThreadLocal专属map,里面可以存放多个ThreadLocal变量,这样就解决了多线程同时操作一个map带来的多线程并发问题。 因为要把ThreadLocal的变量当做全局变量使用,需要把变量与初始化函数写在通用的类中,如DDD领域模型中写在Common模块。 具体的实现如下: public class ThreadLocalUtil { private static ThreadLocal THREAD_LOCAL = new ThreadLocal<>(); public static Integer getScene() { return THREAD_LOCAL.get(); } public static void initScene(Integer scene) { if (THREAD_LOCAL == null) { THREAD_LOCAL = new ThreadLocal<>(); } THREAD_LOCAL.set(scene); } public static void remove() { THREAD_LOCAL.remove(); }} 四、致命点 上面提到了的ThreadLocal会带来内存泄露的问题,深入分析下: 一个ThreadLocal实例对应当前线程的一个对象实例,如果把ThreadLocal声明为某个类的实例变量不是静态变量,那么每次创建一个该类的实例就会导致一个新的对象实例被创建。而这些被创建的实例是同一个类的实例,于是同一个线程可能会访问到同一个类的不同实例,这即使不会导致错误,也会导致重复创建同样的对象。如果使用static修饰后,只要相应的类没有被垃圾回收掉,那么这个类就会持有相对应的ThreadLocal实例引用。 ThreadLocal自身并不存储值,而是作为一个key来让线程从ThreadLocal中获取value。ThreadLocalMap中的key是弱引用,所以jvm在垃圾回收时如果外部没有强引用来引用它,ThreadLocal必然会被回收。但是,作为ThreadLocalMap中的key,ThreadLocal被回收后,ThreadLocalMap就会存在null,但value却不为null。如果当前线程一直不结束或者线程结束后不被你销毁,这会产生内存泄露(已分配空间的堆内存由于某种原因未释放或无法释放导致系统内存浪费或程序运行变慢甚至系统奔溃)。 因此,key弱引用并不是导致内存泄露的原因,而是因为ThreadLocalMap的生命周期与当前线程一样长,并且没有手动删除对应的value。 解决的方法也很简单,只需要打破引用路径中的ThreadLocalMap对对象实例的引用即可。也就是在使用完ThreadLocal之后,必须调用ThreadLocal.remove()。 延伸: 为什么要将Map中的key设置为弱引用呢? 实际上,设置key为弱引用能预防大多数内存泄露的情况。如果key使用强引用,引用的ThreadLocal对象被回收,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,也会导致内存泄露。设置为弱引用后,引用的ThreadLocal对象被回收,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被java GC回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。 一、治理背景 数据库系统性能问题会对应用程序的性能和用户体验产生负面影响。慢查询可能导致应用程序响应变慢、请求堆积、系统负载增加等问题,甚至引发系统崩溃或不可用的情况。慢SQL治理是在数据库系统中针对执行缓慢的SQL查询进行优化和改进的一项重要工作。 但原有的治理节奏,一般在大促备战期间,集中投入人力紧急治理,日常对慢SQL的关注度不高;即使研发团队想着手治理,实例下的SQL明细筛选繁琐,趋势不明,缺少系统化,数字化的治理方案。 所以为了保证系统稳定性,预防潜在慢SQL导致应急事故,发起慢SQL常态化备战专项,下文主要描述专项的实践及落地情况。 Sharding-JDBC Sharding-JDBC关系型数据库的水平扩张框架。 表 goods_0goods_id bigint(20)goods_name varchar(100)goods_type bigint(20) 数据库表中字段goods_type的数值的奇偶进行判断,奇数使用goods_1表,偶数使用goods_0表。在应用程序中我们操作虚拟表goods,但是当真正操作数据库的时候,会根据我们的分库分表规则进行匹配然后操作。 创建表和数据库: CREATE DATABASE database0;use database0;drop table if exists `goods_0`;crate table `goods_0` ( `goods_id` bigint(20) not null, `goods_name` varchar(100) collate utf8_bin not null, `goods_type` bigint(20) default null, primary key(`goods_id`) ) engine = InnoDB default charset = utf8 collate = utf8_bin; drop table exists `goods_1`;create table `goods_1`( `goods_id` bigint(20) not null, `goods_name` varchar(100) collate utf8_bin not null, `goods_type` bigint(20) default null, primary key (`goods_id`)) engine = InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; 加入当当的sharding-jdbc-core依赖和druid连接池 "1.0" encoding="UTF-8"?>"http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot spring-boot-starter-parent 2.0.3.RELEASE com.xx springboot2_shardingjdbc_fkfb 0.0.1-SNAPSHOT springboot2_shardingjdbc_fkfb springboot2_shardingjdbc_fkfb 1.8 org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime mysql mysql-connector-java runtime org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true com.alibaba druid 1.1.9 com.dangdang sharding-jdbc-core 1.5.4 org.springframework.boot spring-boot-maven-plugin ##Jpa配置spring.jpa.database=mysqlspring.jpa.show-sql=truespring.jpa.hibernate.ddl-auto=none ##数据库配置##数据库database0地址database0.url=jdbc:mysql://localhost:3306/database0?characterEncoding=utf8&useSSL=false##数据库database0用户名database0.username=root##数据库database0密码database0.password=root##数据库database0驱动database0.driverClassName=com.mysql.jdbc.Driver##数据库database0名称database0.databaseName=database0 ##数据库database1地址database1.url=jdbc:mysql://localhost:3306/database1?characterEncoding=utf8&useSSL=false##数据库database1用户名database1.username=root##数据库database1密码database1.password=root##数据库database1驱动database1.driverClassName=com.mysql.jdbc.Driver##数据库database1名称database1.databaseName=database1 @SpringBootApplication @EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class}) @EnableTransactionManagement(proxyTargetClass = true) @EnableConfigurationProperties public class Springboot2ShardingjdbcFkfbApplication { public static void main(String[] args) { SpringApplication.run(Springboot2ShardingjdbcFkfbApplication.class, args); } } @Entity @Table(name="goods") @Data public class Goods { @Id private Long goodsId; private String goodsName; private Long goodsType; } @Data@ConfigurationProperties(prefix = "database0")@Componentpublic class Database0Config { private String url; private String username; private String password; private String driverClassName; private String databaseName; public DataSource createDataSource() { DruidDataSource result = new DruidDataSource(); result.setDriverClassName(getDriverClassName()); result.setUrl(getUrl()); result.setUsername(getUsername()); result.setPassword(getPassword()); return result; }} 新建DataSourceConfig用于创建数据源和使用分库分表策略,其中分库分表策略会调用分库算法类和分表算法类,DataSourceConfig类代码如下所示。 @Configurationpublic class DataSourceCoding { @Autowired private Database0Config database0Config; @Autowired private Database1Config database1Config; @Autowired private TableShardingAlgorithm tableShardingAlgorithm; @Bean public DataSource getDataSoure() throws SQLException [ return buildDataSource(); } private DataSource buildDataSource() throws SQLException { // 分库设置 Map dataSourceMap = new HashMap<>(2); // 添加两个数据库 database0 和 database1 dataSourceMap.put(database0Config.getDatabaseName(), database0Config.createDataSource()); dataSourceMap.put(database1Config.getDatabaseName(), database1Config.createDataSource()); // 设置默认数据库 DataSourceRule dataSourceRule = new DataSourceRule(dataSourceMap, database0Config.getDatabaseName()); //分表设置,大致思想就是将查询虚拟表goods根据一定规则映射到真实表中去 TableRule orderTableRule = TableRule.builder("goods").actualTables(Arrays.asList("goods_0", "goods_1")).dataSourceRule(dataSourceRule).build(); //分库分表策略 ShardingRule shardingRule = ShardingRule.builder().dataSourceRule(dataSourceRule).tableRules(Arrays.asList(orderTableRule)).databaseShardingStrategy } @Bean public KeyGenerator keyGenerator() { return new DefaultKeyGenerator(); }} 加群联系作者vx:xiaoda0423 仓库地址:https://github.com/webVueBlog/JavaGuideInterview 相关阅读: k8s的本质 双十一接口流量突增,如何做好性能调优? OpenHD改造实现廉价高清数字图传(树莓派+PC)—(六)OSD和视频画面整合显示 论文分享 | 利用单模态自监督学习实现多模态AVSR Linux 最常用命令 [C++]C++类和对象(下)、 Driver8833电机驱动模块的使用(STM32为主控) 使用 Python 构建自动播客视频,将音频转为视频 ZMQ之自杀的蜗牛模式和黑箱模式 学一点Ceph知识:初识Ceph 原文地址:https://blog.csdn.net/qq_36232611/article/details/136134316 最新文章 攻防演习之三天拿下官网站群 数据安全治理学习——前期安全规划和安全管理体系建设 企业安全 | 企业内一次钓鱼演练准备过程 内网渗透测试 | Kerberos协议及其部分攻击手法 0day的产生 | 不懂代码的"代码审计" 安装scrcpy-client模块av模块异常,环境问题解决方案 leetcode hot100【LeetCode 279. 完全平方数】java实现 OpenWrt下安装Mosquitto AnatoMask论文汇总 【AI日记】24.11.01 LangChain、openai api和github copilot 热门文章 十款代码表白小特效 一个比一个浪漫 赶紧收藏起来吧!!! 奉劝各位学弟学妹们,该打造你的技术影响力了! 五年了,我在 CSDN 的两个一百万。 Java俄罗斯方块,老程序员花了一个周末,连接中学年代! 面试官都震惊,你这网络基础可以啊! 你真的会用百度吗?我不信 — 那些不为人知的搜索引擎语法 心情不好的时候,用 Python 画棵樱花树送给自己吧 通宵一晚做出来的一款类似CS的第一人称射击游戏Demo!原来做游戏也不是很难,连憨憨学妹都学会了! 13 万字 C 语言从入门到精通保姆级教程2021 年版 10行代码集2000张美女图,Python爬虫120例,再上征途 Copyright © 2022 侵权请联系2656653265@qq.com 京ICP备2022015340号-1 正则表达式工具 cron表达式工具 密码生成工具 京公网安备 11010502049817号
声明式事务: 事务由容器进行管理,对于开发人员而言,几乎不管理事务。简单理解就是加个事务注解或做个AOP切面。
@Transactional(rollbackFor = Exception.class)public void updateStatus(String applyNo){ cashierApplyMapper.updateStatus(applyNo, CANCEL_STATUS, CANCEL_STATUS_DESC);}
本地事务模型 自主可控 侵入性较大,开发人员时刻关注事务边界,写大量commit代码,不支持XA
编程式事务模型 模型高了一个层级,自主可控性强 需要写代码,不优雅
声明式事务模型 简单易用 事务边界必须是方法级别,依赖AOP的机制
SQL的全称: Structured Query Language。中文翻译:结构化查询语言
以Spring+Mybatis+JDBC+Mysql为例,常见的事务类请求的调用链路如下图。请求调用应用服务,应用服务中开启事务并进行业务操作,操作过程中调用Mybatis进行数据库类操作,Mybatis通过JDBC驱动与底层数据库交互。
请求方->请求接受入口->Spring事务->业务操作->Mybatis->JDBC->数据库MySql
因此接下来先按Mysql、JDBC、Mybatis、Spring来介绍各层的事务相关知识;最后进行全链路的调用分析。
Mysql事务相关
Mysql逻辑架构
架构图如下(InnoDB存储引擎):
客户端
连接线程处理(连接处理,授权认证,安全)
查询缓存(分析器->优化器->执行器)查询,解析,分析,优化,缓存
InnoDB存储引擎(Buffer Pool缓冲池)(Redo Log Buffer),undo日志文件,磁盘文件,redo日志文件,binlog文件(存储和读取数据)
MySQL事务是由存储引擎实现的,MySQL支持事务的存储引擎有InnoDB、NDB Cluster等,其中InnoDB的使用最为广泛,其他存储引擎如MyIsam、Memory等不支持事务。
Mysql的事务保证
Mysql的4个特性中有3个与 WAL(Write-Ahead Logging,先写日志,再写磁盘)有关系,需要通过 Redo、Undo 日志来保证等,而一致性需要通过DBMS的功能逻辑及原子性、隔离性、持久性共同来保证。
日志机制(Redo/Undo):原子性,隔离性,持久性
DBMS功能逻辑:一致性
首先我们要先根据以上Hutool中提供的脱敏类型来编写我们自己的类型 或者 直接使用DesensitizedUtil中的DesensitizedType
public enum DataMaskingType { /** * 用户ID */ USER_ID, /** * 中文名 */ CHINESE_NAME, /** * 身份证号 */ ID_CARD, /** * 座机 */ FIXED_PHONE, /** * 手机号 */ MOBILE_PHONE, /** * 地址 */ ADDRESS, /** * 邮箱 */ EMAIL, /** * 密码 */ PASSWORD, /** * 中国大陆车牌号 */ CAR_LICENSE, /** * 银行卡号 */ BANK_CARD, /** * IPv4地址 */ IPV4, /** * IPv6地址 */ IPV6, /** * 自定义类型 */ CUSTOM; }
编写自定义注解
@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@JacksonAnnotationsInside@JsonSerialize(using = DataMaskingSerialize.class)public @interface DataMasking { /** * 数据脱敏类型 */ DataMaskingType type() default DataMaskingType.CUSTOM; /** * 脱敏开始位置(包含) */ int start() default 0; /** * 脱敏结束位置(不包含) */ int end() default 0; }
需要注意的是:当DataMaskingType为 CUSTOM 时,才需要填写 start 和 end ,且这两个参数才会生效,且 start 中是包含当前下标的字符的,而 end 不包含当前下标的字符。
编写自定义序列化类
@AllArgsConstructor@NoArgsConstructorpublic class DataMaskingSerialize extends JsonSerializer implements ContextualSerializer { private DataMaskingType type; private Integer start; private Integer end; @Override public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { String value = (String) o; switch (type) { //userId case USER_ID: jsonGenerator.writeString(String.valueOf(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.USER_ID))); break; //中文名 case CHINESE_NAME: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.CHINESE_NAME)); break; //身份证号 case ID_CARD: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.ID_CARD)); break; //座机 case FIXED_PHONE: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.FIXED_PHONE)); break; //手机号 case MOBILE_PHONE: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.MOBILE_PHONE)); break; //地址 case ADDRESS: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.ADDRESS)); break; //邮箱 case EMAIL: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.EMAIL)); break; case BANK_CARD: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.BANK_CARD)); break; //密码 case PASSWORD: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.PASSWORD)); break; //中国大陆车牌号 case CAR_LICENSE: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.CAR_LICENSE)); break; case IPV4: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.IPV4)); break; case IPV6: jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.IPV6)); break; //自定义 case CUSTOM: jsonGenerator.writeString(CharSequenceUtil.hide(value, start, end)); break; default: break; } } @Override public JsonSerializer createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException { if (Objects.nonNull(beanProperty)) { //判断是否为string类型 if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) { DataMasking anno = beanProperty.getAnnotation(DataMasking.class); if (Objects.isNull(anno)) { anno = beanProperty.getContextAnnotation(DataMasking.class); } if (Objects.nonNull(anno)) { return new DataMaskingSerialize(anno.type(), anno.start(), anno.end()); } } return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty); } return serializerProvider.findNullValueSerializer(null); }}
继承于 JsonSerializer 并实现了 ContextualSerializer 中的方法,并对我们自定义注解声明的字段进行拦截和脱敏加密操作
@Data@Builder@ToString@AllArgsConstructor@NoArgsConstructorpublic class TestEntity { @DataMasking(type = DataMaskingType.USER_ID) private Integer userId; @DataMasking(type = DataMaskingType.CHINESE_NAME) private String userName; @DataMasking(type = DataMaskingType.ADDRESS) private String address; @DataMasking(type = DataMaskingType.ID_CARD) private String idCard; @DataMasking(type = DataMaskingType.FIXED_PHONE) private String fixedPhone; @DataMasking(type = DataMaskingType.MOBILE_PHONE) private String mobilePhone; @DataMasking(type = DataMaskingType.EMAIL) private String email; @DataMasking(type = DataMaskingType.PASSWORD) private String password; @DataMasking(type = DataMaskingType.CAR_LICENSE) private String carLicense; @DataMasking(type = DataMaskingType.BANK_CARD) private String bankCard; @DataMasking(type = DataMaskingType.IPV4) private String ipv4; @DataMasking(type = DataMaskingType.IPV6) private String ipv6; @DataMasking(type = DataMaskingType.CUSTOM,start = 3,end = 9) private String custom; /** * 不进行数据脱敏的字段 */ private String noMask; }
@RestControllerpublic class TestController { @GetMapping("/test") public TestEntity test() { return TestEntity.builder() .userId(1234567890) .userName("张三") .password("12") .address("河南省郑xxx区") .email("xxxx@xx.com") .fixedPhone("08xxxxx792") .mobilePhone("138xxxx888") .carLicense("豫P3xxx3") .bankCard("167xxxxx740") .idCard("4127xxxx6677") .ipv4("192xxxx.236") .ipv6("abcd:12xxxxx567:089:0:0000") .custom("2xxxx58794") .noMask("我是不需要数据脱敏的字段") .build(); } }
使用了Hutool的DesensitizedUtil中的 desensitized 方法来实现数据脱敏,在 CUSTOM 类型的脱敏字段中,start 和 end 两个属性是必填的,且 start 包含当前下标,而 end 不包含当前下标。
一、主从架构
为什么我们要进行读写分离?个人觉得还是业务发展到一定的规模,驱动技术架构的改革,读写分离可以减轻单台服务器的压力,将读请求和写请求分流到不同的服务器,分摊单台服务的负载,提高可用性,提高读请求的性能。
一个基础的Mysql的主从架构,1主1备3从。这种架构是客户端主动做的负载均衡,数据库的连接信息一般是放到客户端的连接层,也就是说由客户端来选择数据库进行读写
一个带proxy的主从架构,客户端只和proxy进行连接,由proxy根据请求类型和上下文决定请求的分发路由。 两种架构方案各有什么特点:
1.客户端直连架构,由于少了一层proxy转发,所以查询性能会比较好点儿,架构简单,遇到问题好排查。但是这种架构,由于要了解后端部署细节,出现主备切换,库迁移的时候客户端都会感知到,并且需要调整库连接信息
2.带proxy的架构,对客户端比较友好,客户端不需要了解后端部署细节,连接维护,后端信息维护都由proxy来完成。这样的架构对后端运维团队要求比较高,而且proxy本身也要求高可用,所以整体架构相对来说比较复杂
但是不论使用哪种架构,由于主从之间存在延迟,当一个事务更新完成后马上发起读请求,如果选择读从库的话,很有可能读到这个事务更新之前的状态,我们把这种读请求叫做过期读。出现主从延迟的情况有多种,有兴趣的同学可以自己了解一下,虽然出现主从延迟我们同样也有应对策略,但是不能100%避免,这些不是我们本次讨论的范围,我们主要讨论一下如果出现主从延迟,刚好我们的读走的都是从库,我们应该怎么应对?
首先我把应对的策略总结一下:
强制走主库
sleep方案
判断主从无延迟
等主库位点
等GTID方案
最近接了一个新需求,业务场景上需要在原有基础上新增2个字段,接口新增参数意味着很多类和方法的逻辑都需要改变,需要先判断是否属于该业务场景,再做对应的逻辑。原本的打算是在入口处新增变量,在操作数据的时候进行逻辑判断将变量进行存储或查询。
如果全链路都变更入参和结构,很明显代码上很不优雅,后续如果还要增加业务场景,又需要再改一遍。如果有一个方法可以传递全局变量,而且仅限于当前线程就好了。
到此,会想到有两种解决方案:之前用的比较少的ThreadLocal或者使用redis缓存。考虑到新增字段都是些增删改查的操作,没有必要存到redis中,故使用ThreadLocal。
一、ThreadLocal定义
以微服务架构为例,服务提供方在收到调用方的请求后,会把这个请求分配给一个线程进行处理。一般来说,一个请求会一直由同一个线程处理,中间不会切换线程,所以如果有一个线程中共享的变量,可以当全局变量使用。
ThreadLocal实现的就是一个线程中的全局变量,与真正的全局变量的区别在于ThreadLocal的变量是每个线程中的全局变量,也就是说不同线程访问到的值是不一样的。其填充的变量属于当前线程,该变量对于其他线程是隔离的。
由定义可以发现,ThreadLocal有两个特性:每个Thread的变量只能由当前Thread使用;由于其他线程不可访问,则不存在多线程间共享的问题。
二、修饰
ThreadLocal提供了线程本地的实例,它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。
ThreadLocal变量通常被private static修饰,这样的好处是当一个线程结束时,它所使用的ThreadLocal实例副本都可被回收,避免重复创建。坏处就是这样做可能正好导致内存泄漏。
三、底层实现
ThreadLocal最朴素的内部实现是Map,这是一个HashMap,又称为ThreadLocalMap。但Java源码并不是Map的实现。这是因为如果多个线程访问同一个map,这个map需要是线程安全的,构造比较麻烦。Java采用了更简单粗暴的做法:每个线程都有自己的ThreadLocal专属map,里面可以存放多个ThreadLocal变量,这样就解决了多线程同时操作一个map带来的多线程并发问题。
Map
因为要把ThreadLocal的变量当做全局变量使用,需要把变量与初始化函数写在通用的类中,如DDD领域模型中写在Common模块。
具体的实现如下:
public class ThreadLocalUtil { private static ThreadLocal THREAD_LOCAL = new ThreadLocal<>(); public static Integer getScene() { return THREAD_LOCAL.get(); } public static void initScene(Integer scene) { if (THREAD_LOCAL == null) { THREAD_LOCAL = new ThreadLocal<>(); } THREAD_LOCAL.set(scene); } public static void remove() { THREAD_LOCAL.remove(); }}
四、致命点
上面提到了的ThreadLocal会带来内存泄露的问题,深入分析下:
一个ThreadLocal实例对应当前线程的一个对象实例,如果把ThreadLocal声明为某个类的实例变量不是静态变量,那么每次创建一个该类的实例就会导致一个新的对象实例被创建。而这些被创建的实例是同一个类的实例,于是同一个线程可能会访问到同一个类的不同实例,这即使不会导致错误,也会导致重复创建同样的对象。如果使用static修饰后,只要相应的类没有被垃圾回收掉,那么这个类就会持有相对应的ThreadLocal实例引用。
ThreadLocal自身并不存储值,而是作为一个key来让线程从ThreadLocal中获取value。ThreadLocalMap中的key是弱引用,所以jvm在垃圾回收时如果外部没有强引用来引用它,ThreadLocal必然会被回收。但是,作为ThreadLocalMap中的key,ThreadLocal被回收后,ThreadLocalMap就会存在null,但value却不为null。如果当前线程一直不结束或者线程结束后不被你销毁,这会产生内存泄露(已分配空间的堆内存由于某种原因未释放或无法释放导致系统内存浪费或程序运行变慢甚至系统奔溃)。
因此,key弱引用并不是导致内存泄露的原因,而是因为ThreadLocalMap的生命周期与当前线程一样长,并且没有手动删除对应的value。 解决的方法也很简单,只需要打破引用路径中的ThreadLocalMap对对象实例的引用即可。也就是在使用完ThreadLocal之后,必须调用ThreadLocal.remove()。
延伸:
为什么要将Map中的key设置为弱引用呢?
实际上,设置key为弱引用能预防大多数内存泄露的情况。如果key使用强引用,引用的ThreadLocal对象被回收,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,也会导致内存泄露。设置为弱引用后,引用的ThreadLocal对象被回收,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被java GC回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。
一、治理背景
数据库系统性能问题会对应用程序的性能和用户体验产生负面影响。慢查询可能导致应用程序响应变慢、请求堆积、系统负载增加等问题,甚至引发系统崩溃或不可用的情况。慢SQL治理是在数据库系统中针对执行缓慢的SQL查询进行优化和改进的一项重要工作。
但原有的治理节奏,一般在大促备战期间,集中投入人力紧急治理,日常对慢SQL的关注度不高;即使研发团队想着手治理,实例下的SQL明细筛选繁琐,趋势不明,缺少系统化,数字化的治理方案。
所以为了保证系统稳定性,预防潜在慢SQL导致应急事故,发起慢SQL常态化备战专项,下文主要描述专项的实践及落地情况。
Sharding-JDBC关系型数据库的水平扩张框架。
表 goods_0goods_id bigint(20)goods_name varchar(100)goods_type bigint(20)
数据库表中字段goods_type的数值的奇偶进行判断,奇数使用goods_1表,偶数使用goods_0表。在应用程序中我们操作虚拟表goods,但是当真正操作数据库的时候,会根据我们的分库分表规则进行匹配然后操作。
创建表和数据库:
CREATE DATABASE database0;use database0;drop table if exists `goods_0`;crate table `goods_0` ( `goods_id` bigint(20) not null, `goods_name` varchar(100) collate utf8_bin not null, `goods_type` bigint(20) default null, primary key(`goods_id`) ) engine = InnoDB default charset = utf8 collate = utf8_bin; drop table exists `goods_1`;create table `goods_1`( `goods_id` bigint(20) not null, `goods_name` varchar(100) collate utf8_bin not null, `goods_type` bigint(20) default null, primary key (`goods_id`)) engine = InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
加入当当的sharding-jdbc-core依赖和druid连接池
"1.0" encoding="UTF-8"?>"http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot spring-boot-starter-parent 2.0.3.RELEASE com.xx springboot2_shardingjdbc_fkfb 0.0.1-SNAPSHOT springboot2_shardingjdbc_fkfb springboot2_shardingjdbc_fkfb 1.8 org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime mysql mysql-connector-java runtime org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true com.alibaba druid 1.1.9 com.dangdang sharding-jdbc-core 1.5.4 org.springframework.boot spring-boot-maven-plugin
##Jpa配置spring.jpa.database=mysqlspring.jpa.show-sql=truespring.jpa.hibernate.ddl-auto=none ##数据库配置##数据库database0地址database0.url=jdbc:mysql://localhost:3306/database0?characterEncoding=utf8&useSSL=false##数据库database0用户名database0.username=root##数据库database0密码database0.password=root##数据库database0驱动database0.driverClassName=com.mysql.jdbc.Driver##数据库database0名称database0.databaseName=database0 ##数据库database1地址database1.url=jdbc:mysql://localhost:3306/database1?characterEncoding=utf8&useSSL=false##数据库database1用户名database1.username=root##数据库database1密码database1.password=root##数据库database1驱动database1.driverClassName=com.mysql.jdbc.Driver##数据库database1名称database1.databaseName=database1
@SpringBootApplication @EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class}) @EnableTransactionManagement(proxyTargetClass = true) @EnableConfigurationProperties public class Springboot2ShardingjdbcFkfbApplication { public static void main(String[] args) { SpringApplication.run(Springboot2ShardingjdbcFkfbApplication.class, args); } }
@Entity @Table(name="goods") @Data public class Goods { @Id private Long goodsId; private String goodsName; private Long goodsType; }
@Data@ConfigurationProperties(prefix = "database0")@Componentpublic class Database0Config { private String url; private String username; private String password; private String driverClassName; private String databaseName; public DataSource createDataSource() { DruidDataSource result = new DruidDataSource(); result.setDriverClassName(getDriverClassName()); result.setUrl(getUrl()); result.setUsername(getUsername()); result.setPassword(getPassword()); return result; }}
新建DataSourceConfig用于创建数据源和使用分库分表策略,其中分库分表策略会调用分库算法类和分表算法类,DataSourceConfig类代码如下所示。
@Configurationpublic class DataSourceCoding { @Autowired private Database0Config database0Config; @Autowired private Database1Config database1Config; @Autowired private TableShardingAlgorithm tableShardingAlgorithm; @Bean public DataSource getDataSoure() throws SQLException [ return buildDataSource(); } private DataSource buildDataSource() throws SQLException { // 分库设置 Map dataSourceMap = new HashMap<>(2); // 添加两个数据库 database0 和 database1 dataSourceMap.put(database0Config.getDatabaseName(), database0Config.createDataSource()); dataSourceMap.put(database1Config.getDatabaseName(), database1Config.createDataSource()); // 设置默认数据库 DataSourceRule dataSourceRule = new DataSourceRule(dataSourceMap, database0Config.getDatabaseName()); //分表设置,大致思想就是将查询虚拟表goods根据一定规则映射到真实表中去 TableRule orderTableRule = TableRule.builder("goods").actualTables(Arrays.asList("goods_0", "goods_1")).dataSourceRule(dataSourceRule).build(); //分库分表策略 ShardingRule shardingRule = ShardingRule.builder().dataSourceRule(dataSourceRule).tableRules(Arrays.asList(orderTableRule)).databaseShardingStrategy } @Bean public KeyGenerator keyGenerator() { return new DefaultKeyGenerator(); }}
加群联系作者vx:xiaoda0423
仓库地址:https://github.com/webVueBlog/JavaGuideInterview
京公网安备 11010502049817号