首先我们的环境是只有一台服务器,一个工程的情况,这种情况下使用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锁就实现了。