一、问题描述:
在实际项目的实施过程中,遇到了如下问题:同一套系统,两个人同时对相同的库存批次做出库操作,后提交的事务会覆盖先提交的事务数据,导致库存数据不准确问题。
二、问题分析:
根据对上面的情况进行分析,我们知道这个情况很明显是属于:数据库-并发操作的数据丢失问题,具体分析如下图所示:
通过上图所示,我们了解到,两个事务T1、T2同时读入同一数据并修改,T2提交的结果会破坏或覆盖T1提交的结果,导致T1的修改丢失。
三、解决方案:
针对于上述问题的分析,我们已经明白了问题出现的原因。那么接下来我们该怎么处理呢? 在这里提供以下几种方案供大家交流学习:
方案一:修改数据库的事务隔离级别。常见的4种数据库事务隔离级别(MySQL默认是Repeatable read),既然是事务并发引起的,那我们就使用可串行化(Serializable)的隔离级别,便可以解决以上问题。但是,这种情况大多数情况是不推荐的,因为并发性能低,还可能导致大量超时和锁竞争。
方案二:FOR UPDATE
for update是一种行级锁,也是排他锁。简单说:比如T1事务对A记录数据进行修改,可以在查询A记录的时候通过for update把这条数据进行锁定,这个时候其他事务只可以对A记录进行查询,而不能执行更新操作。当T1事务提交以后,其他事务才可以对其记录进行更新操作。
SELECT * FROM stock WHERE id=1 FOR UPDATE;
方案三:乐观锁-多版本并发控制
简单说:就是在数据库的表中增加“版本号(version)”字段,每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败(也可以使用时间戳作为version的值)。这种方式是比较推荐的,因为使用的是乐观锁,而且效率高。如果在项目初期,而且预估未来系统使用过程中可能存在并发情况的,建议设计系统的时候就做好考虑(如果项目已处于后期,再使用这种方式的话,成本会比较大。因此,论系统设计的重要性!)。
方案四:Redisson分布式锁组件(推荐)
Redisson - 是一个高级的分布式协调Redis客服端,Redis 官方推荐的 Java 客户端有Jedis、Lettuce 和 Redisson。Jedis、Lettuce的API侧重于Redis数据库CRUD(增删改查),而Redisson的API更侧重于分布式处理方案。本文我们使用的是Redisson的提供的分布式锁功能,并且得益于它的WatchDog(看门狗)机制。在这里,我们对于Redisson的原理不做作过多阐述。有兴趣的朋友,可以去Redisson的GitHub官网进行学习:目录 · redisson/redisson Wiki · GitHub
下面我们简单说一下他的使用方式:
1、Maven引入依赖:(具体版本可依据项目需要)
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.0</version>
</dependency>
2、编写Redisson的配置类:
@Configuration
public class RedissonConfig {
@Value("${spring.redis.port}")
private String redisPort;
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.password}")
private String redisPassword;
@Bean
public RedissonClient getRedisson() {
Config config = new Config();
config.useSingleServer() //redis单机模式
.setAddress("redis://"+redisHost+":"+redisPort)
.setDatabase(0)
//.setPassword(redisPassword) //如果redis设置密码,则进行配置
.setConnectionMinimumIdleSize(10)
.setRetryInterval(5000)
.setTimeout(10000)
.setConnectTimeout(10000);
return Redisson.create(config);
}
}
3、在业务逻辑中对数据进行加锁-解锁(在这里我们锁定的是库存的批次号,在表中它是唯一的。锁定的粒度要依据自己的项目实际情况来,但不建议锁定粒度过大)。代码示例如下:
//引入客户端对象
@Autowired
private RedissonClient redissonClient;
//测试例子
public Response test(String batchNo) {
Response response = new Response();
//获取锁
RLock lock = redissonClient.getLock(batchNo);
try {
//加锁
lock.lock();
//处理库存业务逻辑
} catch (Exception e) {
if(Config.isDebug()){
e.printStackTrace();
}
} finally {
//切记,一定要释放锁
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
//解锁
lock.unlock();
}
}
return response;
}
以上便是Redisson锁的一个简单应用,简单说一下流程:在T1操作库存之前,对库存记录001(批次号)做加锁操作,这个时候T2事务就不可以对库存记录001(相同的批次号)再做加锁操作,等待T1事务提交并解锁以后,T2才可以进行加锁操作,从而避免了并发情况下的数据丢失更新问题。
这里再说一下Redisson的WatchDog(看门狗)机制:Redisson提供的分布式锁是支持锁自动续期的(默认续30s 每隔30/3=10 秒续到30s),也就是说,如果锁的时间到了,但线程仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间。
四、写在最后:
以上便是我在工作中遇到的一些问题,以及一些自己的理解和想法,欢迎一起交流学习!