zoukankan      html  css  js  c++  java
  • ThreadLocal的使用场景分析

    目录

    一.ThreadLocal介绍

    二.使用场景1——数据库事务问题

      2.1 问题背景

      2.2 方案1-修改接口传参

      2.3 方案2-使用ThreadLocal

    三.使用场景2——日志追踪问题

    四.其他使用场景

    一.ThreadLocal介绍

      我们知道,变量从作用域范围进行分类,可以分为“全局变量”、“局部变量”两种:

      1.全局变量(global variable),比如类的静态属性(加static关键字),在类的整个生命周期都有效;

      2.局部变量(local variable),比如在一个方法中定义的变量,作用域只是在当前方法内,方法执行完毕后,变量就销毁(释放)了;

      使用全局变量,当多个线程同时修改静态属性,就容易出现并发问题,导致脏数据;而局部变量一般来说不会出现并发问题(在方法中开启多线程并发修改局部变量,仍可能引起并发问题);

      再看ThreadLocal,从名称上就能知道,它可以用来保存局部变量,只不过这个“局部”是指“线程”作用域,也就是说,该变量在该线程的整个生命周期中有效。

    二.使用场景1——数据库事务问题

    2.1问题背景

      下面介绍示例,UserService调用UserDao删除用户信息,涉及到两张表的操作,所以用到了数据库事务:

      数据库封装类DbUtils

    public class DbUtils {
    
        // 使用C3P0连接池
        private static ComboPooledDataSource dataSource = new ComboPooledDataSource("dev");
    
        public static Connection getConnectionFromPool() throws SQLException {
            return dataSource.getConnection();
        }
    
        // 省略其他方法.....
    }
    

      UserService代码如下:  

    public class UserService {
    
        private UserDao userDao;
    
        public void deleteUserInfo(Integer id, String operator) {
            Connection connection = null;
            try {
                // 从连接池中获取一个连接
                connection = DbUtils.getConnectionFromPool();
                // 因为涉及事务操作,所以需要关闭自动提交
                connection.setAutoCommit(false);
    
                // 事务涉及两步操作,删除用户表,增加操作日志
                userDao.deleteUserById(id);
                userDao.addOperateLog(id, operator);
    
                connection.commit();
            } catch (SQLException e) {
                // 回滚操作
                try {
                    if (connection != null) {
                        connection.rollback();
                    }
                } catch (SQLException ex) {
                }
            } finally {
                DbUtils.freeConnection(connection);
            }
        }
    }

      下面是UserDao,省略了部分代码:

    package cn.ganlixin.dao;
    import cn.ganlixin.util.DbUtils;
    import java.sql.Connection;
    
    /**
     * @author ganlixin
     * @create 2020-06-12
     */
    public class UserDao {
    
        public void deleteUserById(Integer id) {
            // 从连接池中获取一个数据连接
            Connection connection = DbUtils.getConnectionFromPool();
    
            // 利用获取的数据库连接,执行sql...........删除用户表的一条数据
    
            // 归还连接给连接池
            DbUtils.freeConnection(connection);
        }
    
        public void addOperateLog(Integer id, String operator) {
            // 从连接池中获取一个数据连接
            Connection connection = DbUtils.getConnectionFromPool();
    
            // 利用获取的数据库连接,执行sql...........插入一条记录到操作日志表
    
            // 归还连接给连接池
            DbUtils.freeConnection(connection);
        }
    }
    

      上面的代码乍一看,好像没啥问题,但是仔细看,其实是存在问题的!!问题出在哪儿呢?就出在从数据库连接池获取连接哪个位置。

      1.UserService会从数据库连接池获取一个连接,关闭该连接的自动提交;

      2.UserService然后调用UserDao的两个接口进行数据库操作;

      3.UserDao的两个接口,都会从数据库连接池获取一个连接,然后执行sql;

      注意,第1步和第3步获得的连接不一定是同一个!!!!这才是关键。

      如果UserService和UserDao获取的数据库连接不是同一个,那么UserService中关闭自动提交的数据库连接,并不是UserDao接口中执行sql的数据库连接,当userService中捕获异常,即使执行rollback,userDao中的sql已经执行完了,并不会回滚,所以数据已经出现不一致!!!

    2.2方案1-修改接口传参

      上面的例子中,因为UserService和UserDao获取的连接不是同一个,所以并不能保证事务原子性;那么只要能够解决这个问题,就可以保证了

      可以修改userDao中的代码,不要每次在UserDao中从数据库连接池获取连接,而是增加一个参数,该参数就是数据库连接,有UserService传入,这样就能保证UserService和UserDao使用同一个数据库连接了

    public class UserDao {
    
        public void deleteUserById(Connection connection, Integer id) {
            // 利用传入的数据库连接,执行sql...........删除用户表的一条数据
        }
    
        public void addOperateLog(Connection connection, Integer id, String operator) {
            // 利用传入的数据库连接,执行sql...........插入一条记录到操作日志表
        }
    }
    

      UserService调用接口时,传入数据库连接,修改代码后如下:

    // 事务涉及两步操作,删除用户表,增加操作日志
    // 新增参数传入数据库连接,保证UserService和UserDao使用同一个连接
    userDao.deleteUserById(connection, id);
    userDao.addOperateLog(connection, id, operator);

      这样做,的确是能解决数据库事务的问题,但是并不推荐这样做,耦合度太高,不利于维护,修改起来也不方便;

    2.3使用ThreadLocal解决

      ThreadLocal可以保存当前线程有效的变量,正好适合解决这个问题,而且改动的点也特别小,只需要在DbUtils获取连接的时候,将获取到的连接存到ThreadLocal中即可:

    public class DbUtils {
    
        // 使用C3P0连接池
        private static ComboPooledDataSource dataSource = new ComboPooledDataSource("dev");
    
        // 创建threadLocal对象,保存每个线程的数据库连接对象
        private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
    
        public static Connection getConnectionFromPool() throws SQLException {
            if (threadLocal.get() == null) {
                threadLocal.set(dataSource.getConnection());
            }
    
            return threadLocal.get();
        }
    
        // 省略其他方法.....
    }
    

      然后UserService和UserDao中,恢复最初的版本,UserService和UserDao中都调用DbUtils获取数据库连接,此时他们获取到的连接则是同一个Connection对象,就可以解决数据库事务问题了。

    三.使用场景2——日志追踪问题

      如果理解了场景1的数据库事务问题,那么对于本小节的日志追踪,光看标题就知道是怎么回事了;

      开发过程时,会在项目中打很多的日志,一般来说,查看日志的时候,都是通过关键字去找日志,这就需要我们在打日志的时候明确的写入某些标识,比如用户ID、订单号、流水号...

      如果业务比较复杂,那么一个请求的处理流程就会比较长,如果将这么一长串的流程给串起来,也可以通过前面说的用户ID、订单号、流水号来串,但有个问题,某些接口没有用户ID或者订单号作为参数!!!!这个时候,当然可以像场景1中给接口增加用户ID或者订单号作为参数,但是这样实现起来,除非想被炒鱿鱼,否则就别这样做。

      此时可以就可以使用ThreadLocal,封装一个工具类,提供唯一标识(可以是用户ID、订单号、或者是分布式全局ID),示例如下:

    package cn.ganlixin.util;
    
    /**
     * 描述:
     * 日志追踪工具类,设置和获取traceId,
     * 此处的traceId使用snowFlake雪花数算法,详情可以参考:https://www.cnblogs.com/-beyond/p/12452632.html
     *
     * @author ganlixin
     * @create 2020-06-12
     */
    public class TraceUtils {
        // 创建ThreadLocal静态属性,存Long类型的uuid
        private static final ThreadLocal<Long> threadLocal = new ThreadLocal<>();
    
        // 全局id生成器(雪花数算法)
        private static final SnowFlakeIdGenerator generator = new SnowFlakeIdGenerator(1, 1);
    
        public static void setUuid(String uuid) {
            // 雪花数算法
            threadLocal.set(generator.nextId());
        }
    
        public static Long getUuid() {
            if (threadLocal.get() == null) {
                threadLocal.set(generator.nextId());
            }
            return threadLocal.get();
        }
    }
    

      

      使用示例:

    @Slf4j
    public class UserService {
    
        private UserDao userDao;
    
        public void deleteUserInfo(Integer id, String operator) {
            log.info("traceId:{}, id:{}, operator:{}", TraceUtils.getUuid(), id, operator);
            
            //.....
        }
    }
    
    @Slf4j
    public class UserDao {
    
        public void deleteUserById(Connection connection, Integer id) {
            log.info("traceId:{}, id:{}", TraceUtils.getUuid(), id);
        }
    }
    

      

     四.其他场景

      其他场景,其实就是利用ThreadLocal“线程私有且线程间互不影响”特性,除了上面的两个场景,常见的还有用来记录用户的登录状态(当然也可以用session或者cookie实现)。

      原文地址:https://www.cnblogs.com/-beyond/p/13111015.html 

  • 相关阅读:
    201521044091《Java程序设计》第7周学习总结
    201521044091《java程序设计》第四次总结
    201521044091 《java程序设计》第八周学习总结
    201521044091 《Java程序设计》第5周学习总结
    201521044091 《Java程序设计》第2周学习总结
    201521044091 《Java程序设计》第3周学习总结
    MySQL设置字符集CHARACTER SET
    Create My MySQL configuration by Percona
    How to use jQuery to manipulate Cookies
    How to use OpenXml to import xml data to Sql server
  • 原文地址:https://www.cnblogs.com/-beyond/p/13111015.html
Copyright © 2011-2022 走看看