前言
接下来讨论一下修改幂等性的第一个方案
数据库幂等性
假设我们有一个 user 表, 每次有人注册就向其中插入一条记录, 我们要保证修改的幂等性, 初步的想法可以有二种: 第一种是先在数据库里查询一下, 如果没有这个 email, 就像数据库里插入一条, 第二种是先删除这个email 的记录然后插入一条新的 email;
我们先假设查询(删除)操作和插入操作不在同一个事务中, 会出现一种情况大概如下:
删除也是类似的情况, 所以单纯的先查(删)后插的方式不能够解决这个问题.
这时候呢, 可能有的小伙伴会发现, 这个可能是由于查询(删除)操作和插入不在同一个事务里导致的, 如果在一个事务里是不是能够解决这个问题呢.
这就涉及到事务隔离级别了, 其中分为读未提交, 读已提交, 可重复读和可序列化, 讲述这四种隔离级别的文章的挺多了, 其中可序列化能解决上面的问题但是性能和并发度不高, 另外应用程序强依赖数据库隔离级别太脆弱了.
乐观锁方案
为了兼顾性能和安全我们可以在建表的时候增加唯一键:
create unique index user_email_uindex on user (email);
情况就会变成下面这样:
比如有些场景没有邮箱这样明显的唯一键, 可以自己手动生成一个, 比如用户打开表单页面就生成一个 uid 返回给前端, 前端重复点击不会导致重提, 还可以根据统一的时间戳或者版本号来防止并发的修改导致的非预期的效果
但是这种方案也存在缺点
- 对数据库有压力, 尤其是冲突可能性过大会导致大量的事务回滚, 性能压力就更大, 其实有点像定义一个方法, 输入一个字符串, 判断是否是整数, 可以直接调用转换方法, 抛出异常就返回不是整数, java 规范中都不建议这么操作, 因为 try-catch 代价太高, 最好是先判断一下是否符合正则等, 判断是不是符合一般的情况, 然后再做转换.
- 场景有限, 需要所需的数据库支持乐观锁方式, 有些操作或者一些非关系型数据库不能支持乐观锁, 这种方式就没法使用
- 业务复杂起来乐观锁考虑的场景就更复杂, ABA 问题等, 比如上面的例子一瞬间, 第一次点击的事务已经注册上去了, 又注销了, 第二次点击产生的事务又注册上了,,, 或者业务逻辑很复杂, 不单单是插入一张表, 还需要发一封邮件, 还需要增加角色配置等, 乐观锁的使用场景就要考虑很多问题, 不然会导致多发邮件等等问题
聊完了乐观锁的缺点, 我们后面的文章讲述下其他方式来保证幂等性尽可能的消除乐观锁带来的问题.