解决多线程多用户频繁操作数据库导致数据异常
背景和问题
在一个高并发的系统中,假如项目中多处涉及频繁的对数据库的数据操作,那么并发就容易导致数据库数据异常。例如:用户1:将数据库中指定数据删除,并将新数据添加到数据库;用户2:同样需要将这一部分数据删除,添加新数据到数据库;
那么就会两种情况:
- 用户1和用户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高并发,如何解决,什么方式解决
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通