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;
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_54401017/article/details/129768305
今日推荐