在我们添加用户时,如果点击过快,可能数据库会出现两个一模一样的数据,这个原因有很多种,
但是我遇到的是因为同时执行了两个线程,同时插入了两个一样的数据,那么我来分享下解决之道!!
![](https://img-blog.csdnimg.cn/img_convert/02477bc10bf44e03f6f6a5a8d97be54c.png)
这里就要引入分布式锁了,如果使用synchronized这个锁的话,就是锁的力度比较强,锁的范围有点大,此处利用的redis中的redisson这个锁进行操作
首先需要引入redis和redisson的相关jar包
首先先分析redis
1. spring-data-redis
使用 spring-boot-starter-data-redis 操作 redis
1.依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.4</version>
</dependency>
2.redis的简单使用:
// 存值
redisTemplate.opsForValue().set("Int",1);
// 取值
String String = (String) redisTemplate.opsForValue().get("String");
其他配置(使支持 RedisTemplate<String,Object>)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* redisTemplate 配置类
*/
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
return redisTemplate;
}
}
2. spring-session-data-redis
使用 spring-session-data-redis 自动同步 session 数据到 redis 中,实现 session 共享
依赖
<!-- https://mvnrepository.com/artifact/org.springframework.session/spring-session-data-redis -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.6.3</version>
</dependency>
配置
spring:
# session 配置,过期时间(分钟)
session:
timeout: 86400
store-type: redis
接下来是redisClient配置文件:
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {
private String host;
private String port;
@Bean
public RedissonClient redissonClient(){
// 创建配置
Config config = new Config();
String redisAdress=String.format("redis://%s:%s",host,port);
//setdatebase是指redis库号
config.useSingleServer().setAddress(redisAdress).setDatabase(3);
//创建实例
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
application.yml
redis:
port: 6379
host: localhost
database: 3
其次就是获取锁,以及制订锁的过期时间,-1表示没时间,锁中间执行你需要执行的命令(此处为我程序中的加入队伍为例,同一个用户不能加入多次同一个队伍),之后需要释放锁,这个锁必须要释放,不然会这个线程会一直执行,redisson中的看门狗机制,如果你的任务没有完成,并且你执行的是定时任务们马上过期,他会自动重置过期时间,直到你的任务完成,不然会一直执行下去,其他线程进不来
RLock lock = redissonClient.getLock("my:join_team");
try {
// 抢到锁并执行
while (true) {
// tryLock (最长取锁时间,超时 false;过期时间,执行时间超过过期时间,锁将释放;时间单位)
// 过期时间使用 -1 可以续期;第一个参数是人走了,下个人进来的时间,0 表示只执行一次
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
System.out.println("getLock: " + Thread.currentThread().getId());
QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
userTeamQueryWrapper.eq("userId", userId);
long hasJoinNum = userTeamService.count(userTeamQueryWrapper);
if (hasJoinNum > 5) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "最多创建和加入 5 个队伍");
}
// 不能重复加入已加入的队伍
userTeamQueryWrapper = new QueryWrapper<>();
userTeamQueryWrapper.eq("userId", userId);
userTeamQueryWrapper.eq("teamId", teamId);
long hasUserJoinTeam = userTeamService.count(userTeamQueryWrapper);
if (hasUserJoinTeam > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户已加入该队伍");
}
// 已加入队伍的人数
long teamHasJoinNum = this.countTeamUserByTeamId(teamId);
if (teamHasJoinNum >= team.getMaxNum()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已满");
}
// 修改队伍信息
UserTeam userTeam = new UserTeam();
userTeam.setUserId(userId);
userTeam.setTeamId(teamId);
userTeam.setJoinTime(new Date());
return userTeamService.save(userTeam);
}
}
} catch (InterruptedException e) {
log.error("doCacheRecommendUser error", e);
return false;
} finally {
// 只能释放自己的锁
if (lock.isHeldByCurrentThread()) {
System.out.println("unLock: " + Thread.currentThread().getId());
lock.unlock();
}
}
}
为什么需要锁呢?
以我的理解,我们每个人都看作一个线程,然后上厕所好比执行应用程序,当应用程序需要执行时,多个线程一拥而上,肯定会出现几个线程同时执行一个程序的情况,会出现脏数据,重复的数据,这就有必要来加一把锁了,加锁就是厕所的门,当应用程序需要执行时,一个厕所只能进入一个人,也就是一个线程,当这个人在上厕所时,其他人只能在外面等着,等这个人上完了其他人才能进去,执行下一个线程,看门狗机制就是说这个人不上完厕所不出来的意思,无限重置过期时间,知道这个人出来为止,这就是我对锁的想法,为了避免一些不必要的麻烦,上线的项目都需要加一些所锁来维持项目的稳定性。