-->

解决多线程多用户频繁操作数据库导致数据异常

背景和问题

在一个高并发的系统中,假如项目中多处涉及频繁的对数据库的数据操作,那么并发就容易导致数据库数据异常。例如:用户1:将数据库中指定数据删除,并将新数据添加到数据库;用户2:同样需要将这一部分数据删除,添加新数据到数据库;
那么就会两种情况:

  1. 用户1和用户2同时删除和添加 结果:出现双倍数据
  2. 用户1或用户2异步 结果:其中一人只添加了部分数据就被另外一个用户删除,然后两者同时添加,导致部分数据重复

那么这种情况要怎么解决呢?

解决方案

在Java中解决并发操作数据库引起的数据异常,通常涉及到事务管理和锁机制。以下是一些常用的策略:

我觉得这些解决方案主要按解决方式可以概括为两类:代码层面解决和数据库层面解决

代码层面

1. 使用事务(Transaction)

确保数据库操作的原子性,可以使用@Transactional注解在Spring框架中管理事务。

@Transactional
public void updateDatabase(int id, String data) {
    // 数据库更新操作
}

2. 使用乐观锁(Optimistic Locking)

通过在数据表中添加版本号或时间戳字段来管理并发,只有版本号或者时间戳大于之前版本或晚于之前时间戳才可以操作。

@Version
private Timestamp timestamp;

3. 使用分布式锁

通过在Redis中添加key-value实现,只有指定key-value存在才可以操作,否则等待,或者退出。

redisTemplate.opsForValue().set(key, value, timeout, unit)

其实乐观锁是通过数据库添加字段实现的,我觉得也可以是数据库层面,但是网上都说是程序层面的,而且代码中确实也要添加字段,所以具体属于哪种,你们自己定夺吧

4. 使用synchronized关键字

synchronized关键字在Java中主要用于实现线程同步,确保多个线程访问共享资源时的数据一致性和线程安全

synchronized关键字的主要作用:
‌- 原子性‌:synchronized能够保证一个操作要么全部执行,要么全部不执行,防止其他线程在操作未完成时介入。

‌- 可见性‌:确保线程在释放锁之前对共享变量的修改对其他线程可见,避免因缓存导致的数据不一致问题。
‌- 有序性‌:保证程序按照代码的顺序执行,防止指令重排导致的问题。

    public synchronized void deposit(double amount) {
        balance += amount;
    }

5. 使用队列或锁

当多个线程并发修改同一记录时,可以通过队列管理顺序,或使用ReentrantLock等锁来控制并发。

private final ReentrantLock lock = new ReentrantLock();
 
public void updateData() {
    lock.lock();
    try {
        // 数据库更新操作
    } finally {
        lock.unlock();
    }
}

6. 异步处理

对于非紧急的数据操作,可以采用异步处理的方式,减少对数据库的并发影响。

数据库层面

1. 使用悲观锁(Pessimistic Locking)

在查询数据时加锁,防止其他事务修改这部分数据。

SELECT * FROM table_name WHERE condition FOR UPDATE;

2. 使用数据库层面的锁

比如行锁、表锁等,确保在并发操作时数据的一致性。
合理设计数据库索引
优化查询效率,确保索引的准确性和高效性。

解决过程

这里由于刚开时,比较着急,就先用比较简单的分布式锁解决了,不过后面又添加了事务

初步解决:分布式锁

思路:向redis中添加key-value,当存在key-value则为有锁,反之则为没锁
Spring 封装了 RedisTemplate 对象来进行对redis的各种操作,它支持所有的 redis 原生的 api。

这里用到的锁比较简单,因此只需要判断key是否存在即可,不过要求项目中不可出现同样的key,否则会出错,例如:3-1-a,因此添加锁用到的命令为:

添加key:

redisTemplate.opsForValue().set(key, value)

判断key是否存在:

redisTemplate.hasKey(key)

删除key:

redisTemplate.delete(key)

较为复杂的功能可以通过给key设置不同的value实现,像我后续的一个功能要求实现线程池指定线程的开启和关闭,显然,Java的线程具有原子性,无法互相干扰,那么就可以通过给key赋值当前时间,从而在需要的时候判断value,实现线程之间的影响

opsForValue拓展

查阅点资料下面总结看下Redis中opsForValue()方法的使用介绍:
要使用 RedisTemplate,必须要先引入它,下面是它的「maven依赖」:

 <!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <!--<version>2.1.4.RELEASE</version>-->
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<!-- fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.68</version>
</dependency>

引入依赖后,我们需要配置 RedisTemplate。比如:Redis 序列化方式配置:

@Configuration public class RedisConfig { 
 // 设置Redis序列化方式,默认使用的JDKSerializer的序列化方式,效率低,这里我们使用 FastJsonRedisSerializer
    @Bean 
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); // key序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer()); // value序列化
        redisTemplate.setValueSerializer(new FastJsonRedisSerializer<>(Object.class)); // Hash key序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer()); // Hash value序列化
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory); return redisTemplate;
    }
}

设置当前的 key 以及 value 值:

redisTemplate.opsForValue().set(key, value)
redisTemplate.opsForValue().set("num","123");

设置当前的 key 以及 value 值并且设置过期时间:

redisTemplate.opsForValue().set(key, value, timeout, unit)
redisTemplate.opsForValue().set("num","123",10, TimeUnit.SECONDS);

//TimeUnit.DAYS //天  
//TimeUnit.HOURS //小时  
//TimeUnit.MINUTES //分钟  
//TimeUnit.SECONDS //秒  
//TimeUnit.MILLISECONDS //毫秒

将旧的 key 设置为 value,并且返回旧的 key(设置 key 的字符串 value 并返回其旧值):

redisTemplate.opsForValue().getAndSet(key, value);

在原有的值基础上新增字符串到末尾:

redisTemplate.opsForValue().append(key, value)

获取字符串的长度:

redisTemplate.opsForValue().size(key)

重新设置 key 对应的值,如果存在返回 false,否则返回 true:

redisTemplate.opsForValue().setIfAbsent(key, value)

设置 map 集合到 redis:

Map valueMap = new HashMap();  
valueMap.put("valueMap1","map1");  
valueMap.put("valueMap2","map2");  
valueMap.put("valueMap3","map3");  
redisTemplate.opsForValue().multiSet(valueMap); 

如果对应的 map 集合名称不存在,则添加否则不做修改:

Map valueMap = new HashMap();  
valueMap.put("valueMap1","map1");  
valueMap.put("valueMap2","map2");  
valueMap.put("valueMap3","map3");  
redisTemplate.opsForValue().multiSetIfAbsent(valueMap); 

通过 increment(K key, long delta) 方法以增量方式存储 long 值(正值则自增,负值则自减):

redisTemplate.opsForValue().increment(key, increment);

批量获取值:(返回传入 key 所存储的值的类型)

public List<String> multiGet(Collection<String> keys) {
    return redisTemplate.opsForValue().multiGet(keys);
}

修改 redis 中 key 的名称:

public void renameKey(String oldKey, String newKey) {
    redisTemplate.rename(oldKey, newKey);
}

如果旧值 key 存在时,将旧值改为新值:

public Boolean renameOldKeyIfAbsent(String oldKey, String newKey) { 
 return redisTemplate.renameIfAbsent(oldKey, newKey);
}

判断是否有 key 所对应的值,有则返回 true,没有则返回 false:

redisTemplate.hasKey(key)

删除单个 key 值:

redisTemplate.delete(key)

批量删除 key:

redisTemplate.delete(keys) //其中keys:Collection<K> keys

设置过期时间:

public Boolean expire(String key, long timeout, TimeUnit unit){
 return redisTemplate.expire(key, timeout, unit);
}

public Boolean expireAt(String key, Date date) { 
 return redisTemplate.expireAt(key, date);
}

返回当前 key 所对应的剩余过期时间:

redisTemplate.getExpire(key);

返回剩余过期时间并且指定时间单位:

public Long getExpire(String key, TimeUnit unit) {
    return redisTemplate.getExpire(key, unit);
}

查找匹配的 key 值,返回一个 Set 集合类型:

public Set<String> getPatternKey(String pattern) { 
    return redisTemplate.keys(pattern);
}

将 key 持久化保存:

public Boolean persistKey(String key) {
    return redisTemplate.persist(key);
}

将当前数据库的 key 移动到指定 redis 中数据库当中:

public Boolean moveToDbIndex(String key, int dbIndex) {
    return redisTemplate.move(key, dbIndex);
}

最终解决:事务+分布式锁

分布式锁上面已经介绍了,下面就介绍事务:

@Transactional和@GlobalTransactional的区别

@Transactional 和 @GlobalTransactional 都是与事务管理有关的注解,但它们在不同的上下文中工作,具有不同的作用。

1、@Transactional

@Transactional 是 Spring 框架提供的一个注解,用于声明一个方法或类需要事务管理。当你在一个方法上使用 @Transactional 注解时,Spring 会为该方法的执行创建一个新的事务(如果当前没有事务的话)或者在现有事务的上下文中执行(如果当前已经存在一个事务的话)。如果方法执行期间发生异常,事务会被回滚,确保数据的完整性和一致性。

@Transactional 主要用于数据库操作,确保相关的数据修改在一个事务中完成。它适用于单一数据库或单一数据源的情况。

2、@GlobalTransactional
@GlobalTransactional 是 Seata 分布式事务框架提供的一个注解。Seata 是一个开源的分布式事务解决方案,用于处理微服务架构中的分布式事务问题。

当你在一个方法上使用 @GlobalTransactional 注解时,Seata 会为该方法的执行创建一个全局事务。这意味着,即使该方法涉及多个微服务或数据库,这些操作也会被视为一个整体的事务。如果其中任何一个操作失败,全局事务都会被回滚,确保跨多个服务或数据库的数据一致性。

与 @Transactional 不同,@GlobalTransactional 主要用于处理分布式事务,确保跨多个服务或数据库的数据一致性。

通过上面介绍可知在多数据库多服务的情况下应用@GlobalTransactional实现事务,seata通过@GlobalTransactional给每个数据库的undo_log表中添加数据存入事务的xid,若发生异常立刻通过xid回滚

具体操作流程如下:

Seata控制分布式事务
   1)、每一个微服务必须创建undo_Log
   2)、安装事务协调器:seate-server
   3)、整合
       1、导入依赖
       2、解压并启动seata-server:
           registry.conf:注册中心配置    修改 registry : nacos
       3、所有想要用到分布式事务的微服务使用seata DataSourceProxy 代理自己的数据源
       4、每个微服务,都必须导入   registry.conf   file.conf
           vgroup_mapping.{application.name}-fescar-server-group = "default"
       5、启动测试分布式事务
       6、给分布式大事务的入口标注@GlobalTransactional
       7、每一个远程的小事务用@Trabsactional

分布式事务讲解和添加

参考:从入门到精通,超强 RedisTemplate 方法详解!
本地事务与分布式事务
Java高并发,如何解决,什么方式解决

posted @   ꧁ʚ星月天空ɞ꧂  阅读(235)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示