Spring Boot 中的多线程事务处理太繁琐?一个自定义注解直接搞定!

2023-10-31

前言

我们开发的时候常常会遇到多线程事务的问题。以为添加了@Transactional注解就行了,其实你加了注解之后会发现事务失效。

原因:数据库连接spring是放在threadLocal里面,多线程场景下,拿到的数据库连接是不一样的,即是属于不同事务。

本文是基于springboot的@Async注解开启多线程,,并通过自定义注解和AOP实现的多线程事务,避免繁琐的手动提交/回滚事务  (CV即用、参数齐全、无需配置)

一、springboot多线程(声明式)的使用方法?

1、springboot提供了注解@Async来使用线程池,具体使用方法如下:

(1) 在启动类(配置类)添加@EnableAsync来开启线程池

(2) 在需要开启子线程的方法上添加注解@Async

注意: 框架默认 ----->   来一个请求开启一个线程,在高并发下容易内存溢出

所以使用时需要配置自定义线程池,如下:

@Configuration
@EnableAsync
public class ThreadPoolTaskConfig {
 
    @Bean("threadPoolTaskExecutor")//自定义线程池名称
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
 
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
 
        //线程池创建的核心线程数,线程池维护线程的最少数量,即使没有任务需要执行,也会一直存活
        executor.setCorePoolSize(16);
 
        //如果设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
        //executor.setAllowCoreThreadTimeOut(true);
 
        //阻塞队列 当核心线程数达到最大时,新任务会放在队列中排队等待执行
        executor.setQueueCapacity(124);
 
        //最大线程池数量,当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
        //任务队列已满时, 且当线程数=maxPoolSize,,线程池会拒绝处理任务而抛出异常
        executor.setMaxPoolSize(64);
 
        //当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
        //允许线程空闲时间30秒,当maxPoolSize的线程在空闲时间到达的时候销毁
        //如果allowCoreThreadTimeout=true,则会直到线程数量=0
        executor.setKeepAliveSeconds(30);
 
        //spring 提供的 ThreadPoolTaskExecutor 线程池,是有setThreadNamePrefix() 方法的。
        //jdk 提供的ThreadPoolExecutor 线程池是没有 setThreadNamePrefix() 方法的
        executor.setThreadNamePrefix("自定义线程池-");
 
        // rejection-policy:拒绝策略:当线程数已经达到maxSize的时候,如何处理新任务
        // CallerRunsPolicy():交由调用方线程运行,比如 main 线程;如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行, (个人推荐)
        // AbortPolicy():该策略是线程池的默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
        // DiscardPolicy():如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常
        // DiscardOldestPolicy():丢弃队列中最老的任务,队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
 
        //设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean,这样这些异步任务的销毁就会先于Redis线程池的销毁
  executor.setWaitForTasksToCompleteOnShutdown(true);
  //设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住。
  executor.setAwaitTerminationSeconds(60);
 
        executor.initialize();
        return executor;
    }
}

开启子线程方法:  在需要开启线程的方法上添加 注解@Async("threadPoolTaskExecutor")即可,其中注解中的参数为自定义线程池的名称。

二、自定义注解实现多线程事务控制

1.自定义注解

本文是使用了两个注解共同作用实现的,主线程当做协调者,各子线程作为参与者

/**
 * 多线程事务注解: 主事务
 *
 * @author zlj
 * @since 2022/11/3
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MainTransaction {
 int value();//子线程数量
}
package com.example.anno;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
/**
 * 多线程事务注解: 子事务
 *
 * @author zlj
 * @since 2022/11/3
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SonTransaction {
    String value() default "";
}

解释:

两个注解都是用在方法上的,须配合@Transactional(rollbackFor = Exception.class)一起使用

@MainTransaction注解 用在调用方,其参数为必填,参数值为本方法中调用的方法开启的线程数,如:在这个方法中调用的方法中有2个方法用@Async注解开启了子线程,则参数为@MainTransaction(2),另外如果未使用@MainTransaction注解,则直接已无多线程事务执行(不影响方法的单线程事务)

@SonTransaction注解 用在被调用方(开启线程的方法),无需传入参数

2.AOP内容

代码如下:

/**
 * 多线程事务
 *
 * @author zlj
 * @since 2022/11/3
 */
@Aspect
@Component
public class TransactionAop {
 
    //用来存储各线程计数器数据(每次执行后会从map中删除)
 private static final Map<String, Object> map = new HashMap<>();
 
 @Resource
 private PlatformTransactionManager transactionManager;
 
 @Around("@annotation(mainTransaction)")
 public void mainIntercept(ProceedingJoinPoint joinPoint, MainTransaction mainTransaction) throws Throwable {
  //当前线程名称
  Thread thread = Thread.currentThread();
  String threadName = thread.getName();
  //初始化计数器
  CountDownLatch mainDownLatch = new CountDownLatch(1);
  CountDownLatch sonDownLatch = new CountDownLatch(mainTransaction.value());//@MainTransaction注解中的参数, 为子线程的数量
  // 用来记录子线程的运行状态,只要有一个失败就变为true
  AtomicBoolean rollBackFlag = new AtomicBoolean(false);
  // 用来存每个子线程的异常,把每个线程的自定义异常向vector的首位置插入,其余异常向末位置插入,避免线程不安全,所以使用vector代替list
  Vector<Throwable> exceptionVector = new Vector<>();
 
  map.put(threadName + "mainDownLatch", mainDownLatch);
  map.put(threadName + "sonDownLatch", sonDownLatch);
  map.put(threadName + "rollBackFlag", rollBackFlag);
  map.put(threadName + "exceptionVector", exceptionVector);
 
  try {
   joinPoint.proceed();//执行方法
  } catch (Throwable e) {
   exceptionVector.add(0, e);
   rollBackFlag.set(true);//子线程回滚
   mainDownLatch.countDown();//放行所有子线程
  }
 
  if (!rollBackFlag.get()) {
   try {
    // sonDownLatch等待,直到所有子线程执行完插入操作,但此时还没有提交事务
    sonDownLatch.await();
    mainDownLatch.countDown();// 根据rollBackFlag状态放行子线程的await处,告知是回滚还是提交
   } catch (Exception e) {
    rollBackFlag.set(true);
    exceptionVector.add(0, e);
   }
  }
  if (CollectionUtils.isNotEmpty(exceptionVector)) {
   map.remove(threadName + "mainDownLatch");
   map.remove(threadName + "sonDownLatch");
   map.remove(threadName + "rollBackFlag");
   map.remove(threadName + "exceptionVector");
   throw exceptionVector.get(0);
  }
 }
 
 @Around("@annotation(com.huigu.common.anno.SonTransaction)")
 public void sonIntercept(ProceedingJoinPoint joinPoint) throws Throwable {
  Object[] args = joinPoint.getArgs();
  Thread thread = (Thread) args[args.length - 1];
  String threadName = thread.getName();
  CountDownLatch mainDownLatch = (CountDownLatch) map.get(threadName + "mainDownLatch");
  if (mainDownLatch == null) {
   //主事务未加注解时, 直接执行子事务
   joinPoint.proceed();//这里最好的方式是:交由上面的thread来调用此方法,但我没有找寻到对应api,只能直接放弃事务, 欢迎大神来优化, 留言分享
   return;
  }
  CountDownLatch sonDownLatch = (CountDownLatch) map.get(threadName + "sonDownLatch");
  AtomicBoolean rollBackFlag = (AtomicBoolean) map.get(threadName + "rollBackFlag");
  Vector<Throwable> exceptionVector = (Vector<Throwable>) map.get(threadName + "exceptionVector");
 
  //如果这时有一个子线程已经出错,那当前线程不需要执行
  if (rollBackFlag.get()) {
   sonDownLatch.countDown();
   return;
  }
 
  DefaultTransactionDefinition def = new DefaultTransactionDefinition();// 开启事务
  def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);// 设置事务隔离级别
  TransactionStatus status = transactionManager.getTransaction(def);
 
  try {
   joinPoint.proceed();//执行方法
 
   sonDownLatch.countDown();// 对sonDownLatch-1
   mainDownLatch.await();// 如果mainDownLatch不是0,线程会在此阻塞,直到mainDownLatch变为0
   // 如果能执行到这一步说明所有子线程都已经执行完毕判断如果atomicBoolean是true就回滚false就提交
   if (rollBackFlag.get()) {
    transactionManager.rollback(status);
   } else {
    transactionManager.commit(status);
   }
  } catch (Throwable e) {
   exceptionVector.add(0, e);
   // 回滚
   transactionManager.rollback(status);
   // 并把状态设置为true
   rollBackFlag.set(true);
   mainDownLatch.countDown();
   sonDownLatch.countDown();
  }
 }
}

扩展说明: CountDownLatch是什么?

一个同步辅助类

  • 创建对象时: 用给定的数字初始化 CountDownLatch

  • countDown() 方法: 使计数减1

  • await() 方法: 阻塞当前线程, 直至当前计数到达零。

本文中:

用 计数 1 初始化的 mainDownLatch 当作一个简单的开/关锁存器,或入口:在通过调用 countDown() 的线程打开入口前,所有调用 await 的线程都一直在入口处等待。

用 子线程数量 初始化的 sonDownLatch 可以使一个线程在 N 个线程完成某项操作之前一直等待,或者使其在某项操作完成 N 次之前一直等待。

3、注解使用Demo

任务方法:

package com.example.demo.service;
 
import com.example.demo.anno.SonTransaction;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
/**
 * @author zlj
 * @since 2022/11/14
 */
@Service
public class SonService {
 
    /**
     * 参数说明:  以下4个方法参数和此相同
     *
     * @param args   业务中需要传递的参数
     * @param thread 调用者的线程, 用于aop获取参数, 不建议以方法重写的方式简略此参数,
     *               在调用者方法中可以以此参数为标识计算子线程的个数作为注解参数,避免线程参数计算错误导致锁表
     *               传参时参数固定为: Thread.currentThread()
     */
    @Transactional(rollbackFor = Exception.class)
    @Async("threadPoolTaskExecutor")
    @SonTransaction
    public void sonMethod1(String args, Thread thread) {
        System.out.println(args + "开启了线程");
    }
 
    @Transactional(rollbackFor = Exception.class)
    @Async("threadPoolTaskExecutor")
    @SonTransaction
    public void sonMethod2(String args1, String args2, Thread thread) {
        System.out.println(args1 + "和" + args2 + "开启了线程");
    }
 
    @Transactional(rollbackFor = Exception.class)
    @Async("threadPoolTaskExecutor")
    @SonTransaction
    public void sonMethod3(String args, Thread thread) {
        System.out.println(args + "开启了线程");
    }
 
    //sonMethod4方法没有使用线程池
    @Transactional(rollbackFor = Exception.class)
    public void sonMethod4(String args) {
        System.out.println(args + "没有开启线程");
    }
}

调用方:

package com.example.demo.service;
 
import com.example.demo.anno.MainTransaction;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
import javax.annotation.Resource;
 
/**
 * @author zlj
 * @since 2022/11/14
 */
@Service
public class MainService {
 
    @Resource
    private SonService sonService;
 
    @MainTransaction(3)//调用的方法中sonMethod1/sonMethod2/sonMethod3使用@Async开启了线程, 所以参数为: 3
    @Transactional(rollbackFor = Exception.class)
    public void test1() {
        sonService.sonMethod1("路飞", Thread.currentThread());
        sonService.sonMethod2("索隆", "山治", Thread.currentThread());
        sonService.sonMethod3("娜美", Thread.currentThread());
        sonService.sonMethod4("罗宾");
    }
 
    /*
     * 有的业务中存在if的多种可能, 每一种走向调用的方法(开启线程的方法)数量如果不同, 这时可以选择放弃使用@MainTransaction注解避免锁表
     * 这时候如果发生异常会导致多线程不能同时回滚, 可根据业务自己权衡是否使用
     */
    @Transactional(rollbackFor = Exception.class)
    public void test2() {
        sonService.sonMethod1("路飞", Thread.currentThread());
        sonService.sonMethod2("索隆", "山治", Thread.currentThread());
        sonService.sonMethod3("娜美", Thread.currentThread());
        sonService.sonMethod4("罗宾");
    }
 
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring Boot 中的多线程事务处理太繁琐?一个自定义注解直接搞定! 的相关文章

  • java try catch 程序流程什么时候中断?

    你好 我对 Java 中的异常处理不太熟悉 所以 正如主题在基本 try catch 块中所述 当我在 Try 块中捕获异常时 程序流程何时中断 try some code that raises an Exception catch Ex
  • 使用 Maven 生成 Eclipse 项目文件

    当我尝试使用生成 Eclipse 项目文件时mvn eclipse eclipse我收到以下错误 插件管理器执行目标时出现内部错误 org apache maven plugins maven eclipse plugin 2 9 SNAP
  • Java 字符串哈希码缓存

    字符串不变性的优点之一是哈希码缓存以实现更快的访问 在这种情况下 如何处理具有相同哈希码的字符串的缓存 在这种情况下它真的能提高性能吗 在这种情况下 如何处理具有相同哈希码的字符串的缓存 被缓存的是字符串的哈希码 它被缓存在私有的int字符
  • 如何使用retrofit2动态设置超时?

    public class Router private static Retrofit retrofit null public Retrofit getRetrofit if retrofit null OkHttpClient clie
  • jvm中本机代码如何转换为机器代码[关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我读过一些文章说 jvm将字节码转换为机器码 jvm将字节码转换为本机代码 jvm 将字节码转换为系统调用 系统调用又由操作系统与硬件
  • 通过 html tidy 提供渲染 jsp 页面

    我有一个在 Glassfish 上运行的 Java 项目 它会呈现一些难看的 HTML 这是使用各种内部和外部 JSP 库的副作用 我想设置某种渲染后过滤器 通过 HTMLTidy 提供最终的 HTML 这样源代码就很好且整洁 有助于调试
  • 帮助我避免 JPA、Hibernate 和 MySQL 的连接超时

    我正在使用 JPA Hibernate 作为提供者 Glassfish 和 MySQL 开发中一切都运行良好 但是当我将应用程序部署到测试服务器并让它运行 大部分空闲 过夜时 我通常会在早上遇到这样的情况 2011 03 09T15 06
  • 如何将抽象工厂与单例模式结合起来? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 我正在用 java 编码 并且对这些模式很陌生 谁能给我一个也使用单例的工厂抽象的例子 这是一个实现类的示例单例模式 这个实现也是线程安全
  • 全静态方法和应用单例模式有什么区别?

    我正在创建一个数据库来存储有关我的网站用户的信息 我正在使用 stuts2 因此使用 Java EE 技术 对于数据库 我将创建一个 DBManager 我应该在这里应用单例模式还是将其所有方法设为静态 我将使用这个 DBManager 进
  • 从字符串生成密钥?

    我需要从字符串生成一个密钥 以便我始终可以从同一字符串创建相同的密钥 具体来说是一个Key对象 这样我就可以用它来创建Cipher进而创建SealedObject 这在 Java 中可行吗 我应该考虑什么类 方法组合才能做到这一点 对于 A
  • Android 游戏偶尔出现延迟

    我正在用 Java 制作一个简单的 Android 游戏 我注意到每 20 40 秒就会出现一些烦人的延迟 首先 我认为它们是由垃圾收集器引起的 但当我检查 LogCat 时 我发现游戏滞后时没有垃圾收集 每当游戏开始滞后时 我都会标记日志
  • c和java语言中的换行符

    现在行分隔符取决于系统 但在 C 程序中我使用 n 作为行分隔符 无论我在 Windows 还是 Linux 中运行它都可以正常工作 为什么 在java中 我们必须使用 n 因为它与系统相关 那么为什么我们在c中使用 n 作为新行 而不管我
  • 强制 Java 最低版本以“java -version:”运行在 Windows 上不起作用

    我想强制应用程序运行的 JVM 最低版本为 1 6 或更高版本 即 1 6 我的理解是 您可以使用 version 命令行参数来执行此操作 我尝试了一下 在Linux下似乎可以正常工作 但在Windows下却不行 LINUX 我在 Linu
  • net.sf.jasperreports.engine.JRRuntimeException:java.io.IOException:无法读取字体数据

    我正在尝试通过 JasperReport 创建 PDF 报告 但读取字体数据时出现问题 我有 jasperreports extension properties 和 ClassPath 中的相关 TTF 文件 这是错误 java io I
  • Java 中的 MP4 容器编写器

    我想找到一个免费的 Java MP4 容器 编写器 我不需要编码器 只需要能够根据预期值写入正确原子的编码器 Bonus对于这样一个库 也可以编写 有效 F4V 我更喜欢纯 Java 解决方案 而不是使用 JNI 或外部可执行文件的解决方案
  • JFrame Glasspane 也优于 JDialog,但不应该

    我有一个带有 Glasspane 的 JFrame 未装饰 该框架打开一个 JDialog 也未装饰 也有一个 glassPane 并隐藏自身 setVisible false Glasspanes 通过 setGlassPane 设置 对
  • 获取包中声明的所有 Java 类的名称

    我正在编写一个功能 它将有助于将类放入我的程序的某个包中 另外 我只想要子类某个类的类 我需要这些类才能调用它们的静态方法 有没有一种自动的方法来做到这一点 如果是的话 速度慢吗 如果我不清楚 我想要的是这样的 ArrayList
  • “___ 中的方法 ___() 是在无法访问的类或接口中定义的”编译错误

    我发现了一个奇怪的编译限制 我无法解释 并且我不明白这个限制的原因 示例1 考虑这些类 In package e1 public class C1 enum E1 A B C public E1 x In package e2 import
  • Java LRU 缓存使用 LinkedList

    堆栈溢出的新手 所以请不要介意我以菜鸟的方式问这个问题 我正在尝试使用链表实现 LRU 缓存 我在这里看到了使用 linkedHashMap 和其他数据结构的其他实现 但对于这种情况 我正在尝试使用链表创建最佳优化版本 正如我在技术期间被问
  • Lucene/Hibernate 搜索锁定异常

    我使用 Hibernate Search 在 Web 应用程序上索引和全文搜索项目 没有问题 来自我的 pom xml

随机推荐

  • 计量经济学及Stata应用 5.12 多元回归的Stata实例

    1 多元回归 regress y x1 x2 x3 reg y x1 x2 x3 2 解释定义 1 右上角 Number of obs 样本容量N F n N F统计量 自由度为k 约束条件 m N K 检验整个方程的联合显著性 Prob
  • C++的new和delete

    一 new和delete 1 在C 中堆内存的分配和释放是通过new和delete 来操作的 他们和C语言的malloc和free有什么区别呢 new的底层也是通过malloc来开辟内存的 new比malloc 多一项功能 就是开辟完内存
  • 数据结构:KMP算法

    给定一个字符串 S 以及一个模式串 P 所有字符串中只包含大小写英文字母以及阿拉伯数字 模式串 P 在字符串 S 中多次作为子串出现 求出模式串 P 在字符串 S 中所有出现的位置的起始下标 KMP算法 数组下标从1开始 额外next数组
  • Java常用代码汇总

    1 字符串有整型的相互转换 String a String valueOf 2 integer to numeric string int i Integer parseInt a numeric string to an int 2 向文
  • 编程题 排序子序列(java实现)

    点进来你就是我的人了博主主页 戳一戳 欢迎大佬指点 欢迎志同道合的朋友一起加油喔 链接 排序子序列 牛牛定义排序子序列为一个数组中一段连续的子序列 并且这段子序列是非递增或者非递减排序的 牛牛有一个长度为n的整数数组A 他现在有一个任务是把
  • 云计算免费视频教程:Bashshell脚本编程详解

    云计算免费视频教程 Bashshell脚本编程详解 Shell本身是一个用C语言编写的程序 它是用户使用Unix Linux的桥梁 用户的大部分工作都是通过Shell完成的 Shell既是一种命令语言 也是一种程序设计语言 作为命令语言 它
  • 使用全局配置处理字段名和属性名不一致的情况

    使用全局配置处理字段名和属性名不一致的情况 若字段名和实体类中的属性名不一致 但是字段名符合数据库的规则 使用 实体类中的属性名符合Java的规则 使用驼峰 此时可通过以下三种方式处理字段名和实体类中的属性的映射关系 a gt 可以通过为字
  • outlook邮箱邮件大小限制_Office Outlook 2010、2013附件大小超过了允许的范围限制三种解决方法图解 – 爱分享...

    在outlook2010 2013中添加附件超过20M 就会提示 附件大小超出了允许的范围 outlook2007的附件默认大小是150M 而outlook2010 2013的是20M 有种说法是10M 也许是版本问题 未验证 也许微软出于
  • 如何下载安装VSCode(2023年9月13日更新)

    与VSCode同步更新 如果喜欢 访问原文章链接 https blog csdn net qq 39124701 article details 129400983 给原帖点赞 收藏 支持原作者 1 示例链接 可直接下载 https vsc
  • STM32外部中断

    STM32外部中断 STM32有19个外部中断 线0 15对应外部IO口的输入中断 线16连接PVD输出 线17连接RTC闹钟事件 线18连接USB唤醒事件 GPIO与中断线的映射关系 GPIOx 0映射到EXTI0 GPIOx 1映射到E
  • 使用Python和OpenCV进行拍摄截图

    使用Python和OpenCV进行拍摄截图 1 效果图 2 原理 3 源码 参考 这篇博客将介绍如何使用OpenCV Python和PyAutoGui库拍摄截图 使用pyautogui 可以轻松地将屏幕截图直接捕捉到磁盘或内存 并且转换为O
  • 基于Transformers的自然语言处理入门【八】-Transformers解决序列标注任务

    基于Transformers的自然语言处理入门 八 Transformers解决序列标注任务 1 序列标注概念 2 常见的token级别分类任务 3 预处理数据 4 微调预训练模型 1 序列标注概念 序列标注 通常也可以看作是token级别
  • maven多model打包,launch4j打exe包,inno setup打安装包

    背景 最近公司要做java的本地化服务 需要在用户的终端部署安装java的服务 目前设计的进行http服务 从而提高云服务的容错性 性能 顺便进行有其他window的模块的连接 因为不是所有的本地业务模块都会打入本地服务 根据不同的需求 选
  • 思科c系列服务器cimc密码,UCS C系列服务器故障排除提示.PDF

    UCS C系列服务器故障排除提示 PDF UCS C系列服务器故障排除提示 目录 简介 先决条件 要求 使用的组件 网络图 规则 背景信息 C系列故障排除提示 获取对TAC的Showtech支持 显示系统事件日志事件 显示传感器读 显示CI
  • Laravel框架开发实践

    Laravel是一款优秀的PHP框架 它的流行不仅仅是因为它的优秀特性 而且它开创了一种全新的编程思路 帮助开发者更加高效地完成Web应用的开发 在学习Laravel开发过程中 我深刻体会到了Laravel框架的简洁 高效和灵活 下面是我的
  • 拉结尔6月21日服务器维护,拉结尔6月23日停服维护公告

    拉结尔手游将在6月23日进行短暂的维护更新哦 不清楚具体更新内容究竟是什么的小伙伴们 接下来就让我们一起来看一下吧 拉结尔6月23日停服维护公告 尊敬的探索者 拉结尔 于6月19日更新了全新周年庆版本 针对近日部分探索者反馈的关于新版本的问
  • Cocos2dx-OpenGL ES2.0教程:你的第一个立方体(5)

    在上篇文章中 我们介绍了VBO索引的使用 使用VBO索引可以有效地减少顶点个数 优化内存 提高程序效率 本教程将带领大家一起走进3D 绘制一个立方体 其实画立方体本质上和画三角形没什么区别 所有的模型最终都要转换为三角形 同时 本文还会介绍
  • Nginx构建反向代理缓存服务器

    防伪码 曾经沧海难为水 除却巫山不是云 代理服务可简单的分为正向代理和反向代理 正向代理 用于代理内部网络对Internet的连接请求 如 NAT 客户端指定代理服务器 并将本来要直接发送给目标Web服务器的HTTP请求先发送到代理服务器上
  • springboot2.0整合logback日志(详细)-禁止logback内部日志

    本文转载自作者 70KG 出处 https www cnblogs com zhangjianbing p 8992897 html 一 近期自己的项目想要一个记录日志的功能 而springboot本身就内置了日志功能 然而想要输入想要的日
  • Spring Boot 中的多线程事务处理太繁琐?一个自定义注解直接搞定!

    前言 我们开发的时候常常会遇到多线程事务的问题 以为添加了 Transactional注解就行了 其实你加了注解之后会发现事务失效 原因 数据库连接spring是放在threadLocal里面 多线程场景下 拿到的数据库连接是不一样的 即是