zoukankan      html  css  js  c++  java
  • spring事务管理中,使用Synchronized修饰事务方法,同步为什么会失效

    首先我们的环境是只有一台服务器,一个工程的情况,这种情况下使用synchronized修饰事务方法,同步效果会失效吗?

    代码演示:

    @Service
    public class TestUserServiceImpl implements TestUserService {
    
        @Autowired
        private UserMapper userMapper;
    
        @Override
        @Transactional
        public synchronized int saveUser(UserEntity user) { // 使用synchronized修饰事务方法
            int result = 0;
            List<UserEntity> list = userMapper.selectUserByName(user.getUserName());
            if (list == null || list.isEmpty()) { // 如果不存在此用户名则保存
                result = userMapper.insertUser(user);
            }
            return result;
        }
    }
    
    @RestController
    @RequestMapping("tuser")
    public class TestUserController {
        @Autowired
        private TestUserService userService;
        /**
         * 保存用户测试方法
         *
         * @return
         * @throws InterruptedException
         */
        @GetMapping("/save")
        public int saveUser() throws InterruptedException {
            int N = 5;
            CountDownLatch countDownLatch = new CountDownLatch(N);
            for (int i = 0; i < N; i++) {
                new Thread(() -> {
                    try {
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        TimeUnit.MILLISECONDS.sleep(100L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    UserEntity u = new UserEntity();
                    u.setUserName("jinghx");
                    u.setUserSex("男");
                    userService.saveUser(u);
                }).start();
                countDownLatch.countDown();
            }
            return 1;
        }
    
    }
    

    在Controller中,我模拟了5个并发请求保存用户的场景,Service中的保存方法使用了synchronized关键字修饰,运行结果如下:
    在这里插入图片描述
    可以看到数据库里面保存了三条相同的数据,这是为什么呢?

    原因:

    众所周知,synchronized是Java提供的一个并发控制的关键字,作用于对象上。主要有两种用法,分别是同步方法(访问对象和clss对象)和同步代码块(需要加入对象),保证了代码的原子性和可见性以及有序性,既然已经使用synchronized修饰了事务方法,为什么还会出现重复保存的情况呢?

    这是因为上述代码中,synchronized锁定的是当前对象,而spring为了进行事务管理会生成一个代理对象去执行事务方法,在事务方法执行前开启事务,执行完成后关闭事务,而开启和完毕事务却没有在同步代码块中,当A线程执行完保存操作后就会去释放锁,而此时还没有提交事务,B线程获取锁后,通过用户名查询,由于数据库隔离级别,不能查询到未提交的数据,所以B线程进行了二次插入操作,等执行完后它们一起提交事务,就会出现脏写这种线程安全问题了。

    解决方法:

    1.不进行事务管理,去除@Transactional注解:

        @Override
        public synchronized int saveUser(UserEntity user) { // 使用synchronized修饰非事务方法
            int result = 0;
            List<UserEntity> list = userMapper.selectUserByName(user.getUserName());
            if (list == null || list.isEmpty()) { // 如果不存在此用户名则保存
                result = userMapper.insertUser(user);
            }
            return result;
        }
    

    此方法虽然能避免脏写,但不推荐,除非你确定不需要进行事务管理。

    2.在非事务方法中调用此事务方法,把synchronized 关键字放在非事务方法上:

    @Service
    public class TestUserServiceImpl implements TestUserService {
    
        @Autowired
        private UserMapper userMapper;
    
        @Autowired
        private TestUserService userService; // 注意此时要使用spring注入代理对象
    
        @Override
        public synchronized int saveUser(UserEntity user) {
            // 使用代理对象调用方法,不能直接调用事务方法,直接调用不能进行事务管理
            return userService.realSaveUser(user);
        }
    
        @Override
        @Transactional
        public int realSaveUser(UserEntity user) {
            int result = 0;
            List<UserEntity> list = userMapper.selectUserByName(user.getUserName());
            if (list == null || list.isEmpty()) { // 如果不存在此用户名则保存
                result = userMapper.insertUser(user);
            }
            return result;
        }
    }
    

    注意:在同一个service类中不要直接在非事务方法里面调用事务方法,否则不会开启事务,应该使用spring的@Autowired注解注入代理对象再使用。

    3.抽取出一个独立方法,改变spring事务的传播机制:

    @Service
    public class TestUserServiceImpl implements TestUserService {
    
        @Autowired
        private UserMapper userMapper;
    
        @Autowired
        private TestUserService userService; // 注意此时要使用spring注入代理对象
    
        @Override
        @Transactional
        public int saveUser(UserEntity user) {
            // 使用代理对象调用方法,不能直接调用事务方法,直接调用不能进行事务管理
            synchronized (this) {
                return userService.realSaveUser(user);
            }
        }
    
        @Override
        @Transactional(propagation = Propagation.REQUIRES_NEW) // 事务传播机制使用REQUIRES_NEW传播机制
        public int realSaveUser(UserEntity user) {
            int result = 0;
            List<UserEntity> list = userMapper.selectUserByName(user.getUserName());
            if (list == null || list.isEmpty()) { // 如果不存在此用户名则保存
                result = userMapper.insertUser(user);
            }
            return result;
        }
    }
    

    REQUIRES_NEW:新建事务,如果当前存在事务,则把当前事务挂起,这个方法会独立提交事务,不受调用者的事务影响,父级异常,它也是正常提交。这种方法跟第二种其实是差不多的,区别就是调用的方法现在也可以进行事务控制了,你可以写更多其他的需要事务控制的代码。

    4.使用redis做锁。

    对redis锁在实际项目中的使用,我也没有什么经验,这里简单模拟一下:

    构造一个redis的工具类:

    public class RedisUtil {
    
        private RedisUtil() {
        }
    
        private static final String LOCK_SUCCESS = "OK";
        private static final String SET_IF_NOT_EXIST = "NX";
        private static final String SET_WITH_EXPIRE_TIME = "PX";
    
        /**
         * 尝试获取分布式锁
         *
         * @param jedis      Redis客户端
         * @param lockKey    锁
         * @param requestId  请求标识
         * @param expireTime 超期时间
         * @return 是否获取成功
         */
        public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
    
            String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
    
            if (LOCK_SUCCESS.equals(result)) {
                return true;
            }
            return false;
    
        }
    
        private static final Long RELEASE_SUCCESS = 1L;
    
        /**
         * 释放分布式锁
         *
         * @param jedis     Redis客户端
         * @param lockKey   锁
         * @param requestId 请求标识
         * @return 是否释放成功
         */
        public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
    
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
    
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        }
    
        private static JedisPool jedisPool = null;
    
        static {
            JedisPoolConfig config = new JedisPoolConfig();
            //控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;
            //如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
            config.setMaxTotal(10);
            //控制一个pool最多有多少个状态为idle(空闲的)的jedis实例。
            config.setMaxIdle(5);
            //表示当borrow(引入)一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛出JedisConnectionException;
            config.setMaxWaitMillis(1000 * 100);
            //在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
            config.setTestOnBorrow(true);
    
            //redis的主机IP地址
            String redisHost = "127.0.0.1";
            //redis的端口号
            Integer redisPort = 6379;
            // redis连接密码
            String password = "jinghx";
            jedisPool = new JedisPool(config, redisHost, redisPort, 1000, password);
        }
    
        /**
         * 获取jedis
         *
         * @return
         */
        public static Jedis getJedis() {
            return jedisPool.getResource();
        }
    
        /**
         * 关闭jedis
         *
         * @param jedis
         */
        public static void closeJedis(Jedis jedis) {
            if (jedis != null) {
                jedis.close();
            }
        }
    
    }
    

    修改Service类中的方法为正常书写方式:

    	@Override
    	@Transactional
        public int saveUser(UserEntity user) {
            int result = 0;
            List<UserEntity> list = userMapper.selectUserByName(user.getUserName());
            if (list == null || list.isEmpty()) { // 如果不存在此用户名则保存
                result = userMapper.insertUser(user);
            }
            return result;
        }
    

    测试Controller:

    @RestController
    @RequestMapping("tuser")
    public class TestUserController {
        @Autowired
        private TestUserService userService;
    
        /**
         * 保存用户测试方法
         *
         * @return
         * @throws InterruptedException
         */
        @GetMapping("/save")
        public int saveUser() throws InterruptedException {
            int N = 5;
            CountDownLatch countDownLatch = new CountDownLatch(N);
            for (int i = 0; i < N; i++) {
                new Thread(() -> {
                    try {
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        TimeUnit.MILLISECONDS.sleep(100L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    Jedis jedis = RedisUtil.getJedis();
                    String lockKey = "username";
                    String keyId = UUID.randomUUID().toString();
                    UserEntity u = new UserEntity();
                    u.setUserName("jinghx");
                    u.setUserSex("男");
                    if (RedisUtil.tryGetDistributedLock(jedis, lockKey, keyId, 200)) { // 如果获取到了redis锁
                        try {
                            userService.saveUser(u);
                        } finally {
                            // 释放锁
                            RedisUtil.releaseDistributedLock(jedis, lockKey, keyId);
                            // 关闭jedis连接
                            RedisUtil.closeJedis(jedis);
                        }
                    }
                }).start();
                countDownLatch.countDown();
            }
            return 1;
        }
    }
    

    这样一个简单的redis锁就实现了。

    一颗安安静静的小韭菜。文中如果有什么错误,欢迎指出。
  • 相关阅读:
    通过PowerShell发送TCP请求
    移动端h5全屏body背景图底部未到底bug
    前端开发工具——utils
    微信公众号网页开发——授权登录,js安全域名,jssdk使用
    移动端开发——移动端遮罩层滚动防穿透body解决方案
    js实现cookie有效期至当次日凌晨
    js获取数组中任意一项
    mysql连接错误,error1251解决方式
    Vue packages version mismatch
    js实现拖动效果
  • 原文地址:https://www.cnblogs.com/c-Ajing/p/13448351.html
Copyright © 2011-2022 走看看