高性能方法级幂等性
1.幂等性
就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品使用约支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条.
2.方案设计思路
在分布式系统条件下,需要针对新增和更新业务进行幂等性设计。对于新增业务来说,会出现一份数据多次保存的问题,因此针对这个问题,一般可以设计为除主键ID外,更复杂的操作以唯一流水ID或者业务Code作为一次请求的正确处理记录的唯一标识,例如:支付场景下,如果用户扣款请求成功后网络异常,再次发起同一流水号请求时,会返回记录存在(状态为已支付的订单)。因此需要每次新增之前需要判断此流水号是否存在,如存在即插入失败,此处:还需要控制分布式环境下多线程并发问题,因此需要结合分布式锁设计和幂等性设计。对于修改来说,具体例子以系统角色的添加和修改为例,代码在后文介绍。
3.解决方案代码
sys_role表结构:
CREATE TABLE sys_role (
id int(11) NOT NULL AUTO_INCREMENT,
code varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色code',
name varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名',
create_time datetime(0) DEFAULT NULL,
update_time datetime(0) DEFAULT NULL,
PRIMARY KEY ( id ) USING BTREE,
INDEX code ( code ) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
幂等顶层接口ISuperService
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.extension.service.IService;
import com.central.common.lock.DistributedLock;
/**
* service接口父类
*
* @author
* @date
*/
public interface ISuperService<T> extends IService<T> {
/**
* 幂等性新增记录
*
* @param entity 实体对象
* @param lock 锁实例
* @param lockKey 锁的key
* @param countWrapper 判断是否存在的条件
* @param msg 对象已存在提示信息
* @return
*/
boolean saveIdempotency(T entity, DistributedLock lock, String lockKey, Wrapper<T> countWrapper, String msg);
/**
* 幂等性新增记录
*
* @param entity 实体对象
* @param lock 锁实例
* @param lockKey 锁的key
* @param countWrapper 判断是否存在的条件
* @return
*/
boolean saveIdempotency(T entity, DistributedLock lock, String lockKey, Wrapper<T> countWrapper);
/**
* 幂等性新增或更新记录
*
* @param entity 实体对象
* @param lock 锁实例
* @param lockKey 锁的key
* @param countWrapper 判断是否存在的条件
* @param msg 对象已存在提示信息
* @return
*/
boolean saveOrUpdateIdempotency(T entity, DistributedLock lock, String lockKey, Wrapper<T> countWrapper, String msg);
/**
* 幂等性新增或更新记录
*
* @param entity 实体对象
* @param lock 锁实例
* @param lockKey 锁的key
* @param countWrapper 判断是否存在的条件
* @return
*/
boolean saveOrUpdateIdempotency(T entity, DistributedLock lock, String lockKey, Wrapper<T> countWrapper);
}
注:DistributedLock为分布式锁实现,后续会贴
幂等实现类SuperServiceImpl
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.TableInfoHelper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.central.common.exception.IdempotencyException;
import com.central.common.exception.LockException;
import com.central.common.lock.DistributedLock;
import com.central.common.service.ISuperService;
import java.io.Serializable;
import java.util.Objects;
/**
* service实现父类
*
* @author
* @date
*/
public class SuperServiceImpl<M extends BaseMapper<T>, T> extends ServiceImpl<M, T> implements ISuperService<T> {
/**
* 幂等性新增记录
* 例子如下:
* String username = sysUser.getUsername();
* boolean result = super.saveIdempotency(sysUser, lock
* , LOCK_KEY_USERNAME+username
* , new QueryWrapper<SysUser>().eq("username", username));
*
* @param entity 实体对象
* @param lock 锁实例
* @param lockKey 锁的key
* @param countWrapper 判断是否存在的条件
* @param msg 对象已存在提示信息
* @return
*/
@Override
public boolean saveIdempotency(T entity, DistributedLock lock, String lockKey, Wrapper<T> countWrapper, String msg) {
if (lock == null) {
throw new LockException("DistributedLock is null");
}
if (StrUtil.isEmpty(lockKey)) {
throw new LockException("lockKey is null");
}
try {
//加锁
boolean isLock = lock.lock(lockKey);
if (isLock) {
//判断记录是否已存在
int count = super.count(countWrapper);
if (count == 0) {
return super.save(entity);
} else {
if (StrUtil.isEmpty(msg)) {
msg = "已存在";
}
throw new IdempotencyException(msg);
}
} else {
throw new LockException("锁等待超时");
}
} finally {
lock.releaseLock(lockKey);
}
}
/**
* 幂等性新增记录
*
* @param entity 实体对象
* @param lock 锁实例
* @param lockKey 锁的key
* @param countWrapper 判断是否存在的条件
* @return
*/
@Override
public boolean saveIdempotency(T entity, DistributedLock lock, String lockKey, Wrapper<T> countWrapper) {
return saveIdempotency(entity, lock, lockKey, countWrapper, null);
}
/**
* 幂等性新增或更新记录
* 例子如下:
* String username = sysUser.getUsername();
* boolean result = super.saveOrUpdateIdempotency(sysUser, lock
* , LOCK_KEY_USERNAME+username
* , new QueryWrapper<SysUser>().eq("username", username));
*
* @param entity 实体对象
* @param lock 锁实例
* @param lockKey 锁的key
* @param countWrapper 判断是否存在的条件
* @param msg 对象已存在提示信息
* @return
*/
@Override
public boolean saveOrUpdateIdempotency(T entity, DistributedLock lock, String lockKey, Wrapper<T> countWrapper, String msg) {
if (null != entity) {
Class<?> cls = entity.getClass();
TableInfo tableInfo = TableInfoHelper.getTableInfo(cls);
if (null != tableInfo && StringUtils.isNotEmpty(tableInfo.getKeyProperty())) {
Object idVal = ReflectionKit.getMethodValue(cls, entity, tableInfo.getKeyProperty());
if (StringUtils.checkValNull(idVal) || Objects.isNull(getById((Serializable) idVal))) {
if (StrUtil.isEmpty(msg)) {
msg = "已存在";
}
return this.saveIdempotency(entity, lock, lockKey, countWrapper, msg);
} else {
return updateById(entity);
}
} else {
throw ExceptionUtils.mpe("Error: Can not execute. Could not find @TableId.");
}
}
return false;
}
/**
* 幂等性新增或更新记录
* 例子如下:
* String username = sysUser.getUsername();
* boolean result = super.saveOrUpdateIdempotency(sysUser, lock
* , LOCK_KEY_USERNAME+username
* , new QueryWrapper<SysUser>().eq("username", username));
*
* @param entity 实体对象
* @param lock 锁实例
* @param lockKey 锁的key
* @param countWrapper 判断是否存在的条件
* @return
*/
@Override
public boolean saveOrUpdateIdempotency(T entity, DistributedLock lock, String lockKey, Wrapper<T> countWrapper) {
return this.saveOrUpdateIdempotency(entity, lock, lockKey, countWrapper, null);
}
}
具体业务SysRoleServiceImpl
import java.util.List;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.central.common.constant.CommonConstant;
import com.central.common.lock.DistributedLock;
import com.central.common.model.*;
import com.central.common.service.impl.SuperServiceImpl;
import org.apache.commons.collections4.MapUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.central.user.mapper.SysRoleMapper;
import com.central.user.mapper.SysRoleMenuMapper;
import com.central.user.mapper.SysUserRoleMapper;
import com.central.user.service.ISysRoleService;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Resource;
/**
* @author
*/
@Slf4j
@Service
public class SysRoleServiceImpl extends SuperServiceImpl<SysRoleMapper, SysRole> implements ISysRoleService {
private final static String LOCK_KEY_ROLECODE = CommonConstant.LOCK_KEY_PREFIX+"rolecode:";
@Resource
private SysUserRoleMapper userRoleMapper;
@Resource
private SysRoleMenuMapper roleMenuMapper;
@Autowired
private DistributedLock lock;
@Transactional(rollbackFor = Exception.class)
@Override
public void saveRole(SysRole sysRole) {
String roleCode = sysRole.getCode();
super.saveIdempotency(sysRole, lock
, LOCK_KEY_ROLECODE+roleCode, new QueryWrapper<SysRole>().eq("code", roleCode), "角色code已存在");
}
@Override
@Transactional
public Result saveOrUpdateRole(SysRole sysRole) {
if (sysRole.getId() == null) {
this.saveRole(sysRole);
} else {
baseMapper.updateById(sysRole);
}
return Result.succeed("操作成功");
}
}
SysRole Model类
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author
* 角色
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("sys_role")
public class SysRole extends SuperEntity {
private static final long serialVersionUID = 4497149010220586111L;
private String code;
private String name;
private Long userId;
}
SuperEntity
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.Date;
/**
* 实体父类
*
* @author
*/
@Setter
@Getter
public class SuperEntity<T extends Model<?>> extends Model<T> {
/**
* 主键ID
*/
@TableId
private Long id;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
@Override
protected Serializable pkVal() {
return this.id;
}
}