文章目录

  • 背景
  • 方案
  • 策略一:唯一索引机制
  • 策略二: 分布式锁
  • 策略三:缓存计数器


背景

在开发中很容易忽略接口防重的场景,比如交易中支付防重问题、营销系统中C端领取优惠券接口防重等等处理不当很容易引起资损。

方案

主流方式有三种策略:唯一索引机制、分布式锁和缓存计数器。

策略一:唯一索引机制

新建一个表用于存储锁的记录,

CREATE TABLE `Lock` (
	`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
	`type` int(11) NOT NULL COMMENT '锁类型',
	`businessId` int(11) NOT NULL DEFAULT '0' COMMENT '业务ID',
	`status` int(11) NOT NULL DEFAULT '0' COMMENT '锁状态',
	PRIMARY KEY (`id`),
	UNIQUE KEY (`businessId`, `type`)
);

整体处理流程如下:

  • 线程一插入一条Lock记录数据,成功,相当于获取锁
  • 线程二尝试插入一条Lock记录数据,由于唯一索引的限制,失败
  • 线程一处理完业务,删除此条记录,相当于释放锁

该种方式中若业务代码中包含唯一索引的记录插入操作,也可以直接采用插入记录的唯一索引

策略二: 分布式锁

分布式锁策略采用的是同一个业务ID的不同线程对锁的争用情况来达到防重的目的,示例代码如下:

public void test() {
    if(!redis.setnx(key, value, time)){
        // 尝试获取锁,获取失败直接返回
        return;
    }
    
    try {
        //处理业务中.....
    } finally {
        // 处理完业务,释放锁
        redis.del(key);
    }
}
  • 线程一设置key成功,相当于获取锁,开始处理业务
  • 线程二来请求,key已经设置过了,设置失败,返回
  • 线程一处理完业务,释放锁
策略三:缓存计数器

缓存计数器采用的策略类似于分布式锁的实现,缓存计数器把同一个业务ID每次请求的次数放在缓存(如redis)中,示例代码如下:

public void test() {
    if(redis.incr(key) > 1){
        // 尝试获取锁,获取失败直接返回
        return;
    }
    
    try {
        //处理业务中.....
    } finally {
        // 处理完业务,释放锁
        redis.del(key);
    }
}
  • 线程一对业务ID进行加1后发现等于1,处理业务。
  • 线程二对业务ID进行加1后发现大于1,说明有操作在进行返回。
  • 线程一处理完业务,删除key,即释放锁