既然并发的新增操作会产生问题,那么并发的更新操作是否会有问题呢?
解决方法
解决并发新增
1. 数据库唯一索引(UNIQUE INDEX)
在数据库建表的时候,通过对具有唯一性的字段(比如上述的设备唯一标识)创建唯一索引,或对组合起来后就具备唯一性的几个字段创建联合唯一索引。
这样在并发新增时,只要有一个新增成功,其他的新增操作都会因为数据库抛出的异常(java.sql.SQLIntegrityConstraintViolationException)而失败,我们只需要处理好新增失败的情况就行了。
注意唯一索引的字段需要非空,因为字段值为空时会导致唯一索引约束失效
2. java分布式锁
通过在程序中引入分布式锁,在进行新增操作前需要先获取分布式锁,获取成功才能继续,否则新增失败。
这样也能解决并发插入带来的数据重复问题,只是引入分布式锁的同时也增加了系统的复杂性,如果要落库的数据上有唯一性字段的话,还是推荐采用唯一索引的方法。
在构建分布式锁的过程中,我们需要用到Redis,这里以设备注册时使用的分布式锁为例。
分布式锁简单问答:
Q:锁究竟是什么?
A:锁实质上是存储在Redis中,基于特定规则生成的一个字符串(示例里是固定前缀+设备唯一标识),相当于每个设备注册的时候都有自己对应的一把锁,因为锁只有一把,即使该设备有多个相同的注册请求同时到来,也只有其中获取到那把锁的那一个请求能成功走下去。
Q:什么是获取锁?
A:同一个设备,基于相同的规则生成的字符串(后文以Key代称该字符串)总是相同的,在执行新增操作前,先去Redis中查询这个Key是否存在,如果已存在,就意味着获取锁失败;如果不存在,就将这个Key现存到Redis中,如果存储成功,表示获取锁成功,如果存储失败,还是意味着获取锁失败。
Q:锁是怎么工作的?
A:前面说过,同一个设备,基于相同的规则生成的字符串(Key)总是相同的,在当前线程执行新增操作前,先在Redis中查询这个Key是否存在,如果已存在,表示此时已经有别的线程成功获取了锁,正在做当前线程想要做的新增操作,则当前线程不需要进行后续操作了(是的,你是多余的)
当这个Key不存在时,表示现在还没有其他线程获得锁,则当前线程可以继续进行下一步操作——在Redis中赶紧存入这个Key,当这个Key存储失败时,意味着有别的线程抢先存入了Key成功获取了锁,当前线程晚了一步,想做的工作被别人抢先做了(当前线程可以退下了)
当且仅当在Redis中存入这个Key也成功时,表示当前线程终于获取锁成功,可以安心进行后面的新增操作了,期间别的想做相同新增操作的线程因为获取不到锁,只能全都退场拜拜👋,当前线程执行完后要记得释放锁(从Redis中删除这个Key)。
注册时使用的分布式锁代码如下:
public class LockUtil {
// 对redis底层set/get方法进行了简单封装的工具类
@Autowired
private RedisService redisService;
// 生成锁的固定前缀,从配置文件读取值
@Value(“${redis.register.prefix}”)
private String REDIS_REGISTER_KEY_PREFIX;
// 锁过期时间:即获取锁后线程能进行操作的最长时间,超过该时间后锁自动被释放(失效),别人可以重新开始获取锁进行对应操作
// 设定锁过期时间是为了防止某线程成功获取锁后在执行任务过程中发生意外挂掉了造成锁永远无法被释放
@Value(“${redis.register.timeout}”)
private Long REDIS_REGISTER_TIMEOUT;
/**
-
获取设备注册时的分布式锁
-
@param deviceMacAddress 设备的Mac地址
-
@return
*/
public boolean getRegisterLock(String deviceMacAddress) {
if (StringUtils.isEmpty(deviceMacAddress)) {
return false;
}
// 获取设备对应锁的字符串(Key)
String redisKey = getRegisterLockKey(deviceMacAddress);
// 开始尝试获取锁
// 如果当前任务锁key已存在,则表示当前时间内有其他线程正在对该设备执行任务,当前线程可以退下了
if (redisService.exists(redisKey)){
return false;
}
// 开始尝试加锁,注意此处需使用SETNX指令(因为可能存在多个线程同时到达这一步开始加锁,使用SETNX来确保有且仅有一个设置成功返回)
boolean setLock = redisService.setNX(redisKey, null);
// 开始尝试设置锁过期时间,到了过期时间线程还没有释放锁的话,由保存锁的Redis来确保锁最终被释放,以免出现死锁
// 锁过期时间的设置上,可以评估线程执行任务的正常用时,在正常用时的基础上稍微再大一点
boolean setExpire = redisService.expire(redisKey, REDIS_REGISTER_TIMEOUT);
// 设置锁和设置过期时间均成功时才认为当前线程获取锁成功,否则认为获取锁失败
if (setLock && setExpire) {
return true;
}
// 当发生设置锁成功,但设置过期时间失败的情况时,手动清除刚刚设置的锁Key
redisService.del(redisKey);
return false;
}
/**
-
删除设备注册时的分布式锁
-
@param deviceMacAddress 设备的Mac地址
*/
public void delRegisterLock(String deviceMacAddress) {
redisService.del(getRegisterLockKey(deviceMacAddress));
}
/**
-
获取设备注册时分布式锁的key
-
@param deviceMacAddress 设备mac地址(每个设备的mac地址都是唯一的)
-
@return
*/
private String getRegisterLockKey(String deviceMacAddress) {
return REDIS_REGISTER_KEY_PREFIX + “_” + deviceMacAddress;
}
}
在正常的注册逻辑中使用锁的示例如下:
public ReturnObj registry(@RequestBody String device){
Devices deviceInfo = JSON.parseObject(device, Devices.class);
// 开始注册前加锁
boolean registerLock = lockUtil.getRegisterLock(deviceInfo.getMacAddress());
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
总结
我们总是喜欢瞻仰大厂的大神们,但实际上大神也不过凡人,与菜鸟程序员相比,也就多花了几分心思,如果你再不努力,差距也只会越来越大。实际上,作为程序员,丰富自己的知识储备,提升自己的知识深度和广度是很有必要的。
Mybatis源码解析
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!
就多花了几分心思,如果你再不努力,差距也只会越来越大。实际上,作为程序员,丰富自己的知识储备,提升自己的知识深度和广度是很有必要的。
Mybatis源码解析
[外链图片转存中…(img-K6x6WLJP-1712125510122)]
[外链图片转存中…(img-rmGQJUs2-1712125510122)]
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!