高性能方法級幂等性
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;
}
}