java数据存不存在用什么方法_JAVA处理数据不存在插入存在更新

最近在做项目的时候碰到这样一个问题,做一个用户余额的需求。具体如下:

类似这样一张表:

CREATE TABLE `test_insert` (

`id` int(10) unsigned NOT NULL AUTO_INCREMENT,

`token` varchar(10) NOT NULL DEFAULT '0' COMMENT '用户标志-唯一索引',

`remark` varchar(20) NOT NULL DEFAULT '' COMMENT '备注描述',

`balance` int(10) NOT NULL DEFAULT '0' COMMENT '余额',

`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',

PRIMARY KEY (`id`),

UNIQUE KEY `idx_a` (`token`)

)

用户每次下单需要根据token查看用户在该表中是否有数据,若果有,就把它的balance增加相应的金额amount;如果没有就要在该表新增一条该用户的数据添加相应的余额。

但是在编码的时候注意到一个问题,由于我们系统中的并发是相当大的(日均达到近千万笔交易),对于一个用户也有比较高的并发情况。假设某个用户在表中没有记录,首笔交易还是并发过来的,都去进行insert操作会出问题。为防止这种情况,最开始的编码如下:

@Service

public class TestServiceImpl implements TestService {

private static final String REDISKEY = "balance.insert.";

@Autowired

private StringRedisTemplate stringRedisTemplate;

@Autowired

private TestInsertDAO testInsertDAO;

@Override

public boolean addBalance(String token, Integer amount) {

TestInsertDO testInsertDO = testInsertDAO.selectByToken(token);

Long update = 0L;

if (testInsertDO == null) {

Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(REDISKEY + token, "1");

stringRedisTemplate.expire(REDISKEY + token, 5, TimeUnit.SECONDS);

if (aBoolean) {

testInsertDO = new TestInsertDO();

testInsertDO.setToken(token);

testInsertDO.setRemark("111");

testInsertDO.setBalance(amount);

update = testInsertDAO.insert(testInsertDO); //不存在就插入,balance是该笔订单的amount

stringRedisTemplate.delete(REDISKEY + token);

} else {

update = testInsertDAO.updateByToken(token, amount); //拿不到锁就直接更新

}

} else {

update = testInsertDAO.updateByToken(token, amount); //存在就直接更新

}

return update > 0;

}

}

注意这里的updateByToken方法是在sql上做加操作:

UPDATE

test_insert

SET

BALANCE = BALANCE + #{amount}

WHERE

token = #{token,jdbcType=VARCHAR}

这种方式只是利用了一下redis的分布式锁NX锁

Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(REDISKEY + token, “1”);

来防止有两个线程同时去做插入操作,但是这个写法问题非常多:

这个锁不是阻塞的,并发时有一个拿到了锁去走insert,另一个没拿到走了update,如果update走的比insert快就会造成update操作更新不到数据

stringRedisTemplate.delete(REDISKEY + token);这个解锁操作。如果解锁了后,正巧有个线程这个时候去拿锁,那它拿到了锁后又去执行insert操作,由于token有唯一索引会报错!

并发10笔的情况下,我们来看一下并发执行结果:

2b86d34eebb540b7d81fe5ec24f75312.png224c032af7a4d9fc764862fbad489195.png

数据库只加了9次,并且代码有一次因为唯一索引重复插入问题的报错!

对于这种方式,想了不少方法直接来改进但是都不是太好的方式,需要线程休眠或者其他的锁类型!

下面介绍几种方式来做优化:

1、利用mysql的on duplicate key update

网上百度了一下差不多都在推荐这种方式,只需要把insert语句改一下

INSERT INTO TEST_INSERT(

TOKEN

,REMARK

,BALANCE

)VALUES(

#{token,jdbcType=INTEGER}

, #{remark,jdbcType=VARCHAR}

, #{balance,jdbcType=INTEGER}

)on duplicate key update BALANCE = BALANCE + #{amount}

这种方式的确最简单,但是在实践开发中并不建议使用这样的sql语句,首先这个是mysql特有的一个语句,可能有的持久层框架并不支持,另外这样的语句并不适合DBA来维护,其次,这种写法也有出现死锁的风险,详情参见:https://blog.csdn.net/pml18710973036/article/details/78452688

2、利用成熟的阻塞锁,比如redisson的

上面利用Jedis的分布式锁做的并发处理,其实最大的问题就是Jedis的NX锁是非阻塞的。利用redisson的阻塞锁可以更好地解决这个问题,编码如下:

public boolean addBalance(String token, Integer amount) {

TestInsertDO testInsertDO = testInsertDAO.selectByToken(token);

Long update = 0L;

if (testInsertDO == null) {

RLock lock = redisson.getLock("balanceinsert" + token);

lock.lock(5, TimeUnit.SECONDS);

testInsertDO = testInsertDAO.selectByToken(token); //双重检查;防止加锁过程中被插入数据

if (testInsertDO == null) {

testInsertDO = new TestInsertDO();

testInsertDO.setToken(token);

testInsertDO.setRemark("加余额");

testInsertDO.setBalance(amount);

update = testInsertDAO.insert(testInsertDO); //不存在就插入,balance是该笔订单的amount

} else {

update = testInsertDAO.updateByToken(token, amount); //有数据了就更新

}

lock.unlock();

} else {

update = testInsertDAO.updateByToken(token, amount); //存在就直接更新

}

return update > 0;

}

这里在获得锁之后再去检查一遍数据库的信息,防止在拿锁的过程中有信息被插入了!

关于redisson的分布式锁可以这个:https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers

3、利用唯一索引先插入默认值

这种方式不需要额外的工具来处理。上面的inert的时候把当前订单的余额一起插入了。现在我们分两步,先insert一个默认值,然后再update上该笔订单的amount,把insert异常捕捉,那么在并发时由于唯一索引的存在,一定是只有一条插入成功,其他的报错,捕捉异常后继续走update。

public boolean addBalance(String token, Integer amount) {

TestInsertDO testInsertDO = testInsertDAO.selectByToken(token);

Long update = 0L;

if (testInsertDO == null) {

try {

testInsertDO = new TestInsertDO();

testInsertDO.setToken(token);

testInsertDO.setBalance(0);

testInsertDO.setRemark("加余额");

update = testInsertDAO.insert(testInsertDO); //不存在就插入,balance是该笔订单的amount

} catch (Exception e) {

e.printStackTrace();

}

}

update = testInsertDAO.updateByToken(token, amount); //存在就直接更新

return update > 0;

}

并发10条执行结果:

c4715c5a75e61f560fb1b34e4302f7c3.png

其实这种方式最简单易行,我们最后也采用的这种方式!其实就是把一步拆为2步。像这种并发的解决方案其实经常会遇到,多思考一下方案,跟同事多交流一下,发现代码世界还是无穷无尽的!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值