前言
本节内容是关于使用分布式锁解决并发访问“超卖”问题的最终篇,在前面的章节中我们介绍了使用mysql的行锁、乐观锁、悲观锁解决并发访问导致的超卖问题,存在的问题是行锁、乐观锁、悲观锁不太灵活,需要和具体的业务耦合到一起,不利于使用,本节内容我们通过mysql的唯一索引特性,数据不能重复插入,达到独占排它的基本要求,实现通用的分布式锁功能。
正文
- 创建表app_lock,用于实现分布式锁
- app_lock表
CREATE TABLE `app_lock` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', `lock_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '锁名称', `class_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '类名', `method_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '方法名称', `server_ip` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '服务器IP', `thread_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '线程名称', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `expire_time` datetime DEFAULT NULL COMMENT '过期时间', `remark` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`), UNIQUE KEY `idx_lock_name` (`lock_name`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='应用锁';
PS:lock_name为锁名称,建立唯一索引,保证锁的唯一性,server_ip主机信息和thread_name线程号,可以将分布式锁扩展为可重入锁,expire_time过期时间,可通过定时任务或者消息中间键,自动释放锁。
- 表app_lock实体类创建
package com.ht.atp.plat.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.time.LocalDateTime;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
@Data
@TableName("app_lock")
@ApiModel(value = "AppLock对象", description = "应用锁")
public class AppLock implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@ApiModelProperty("锁名称")
@TableField("lock_name")
private String lockName;
@ApiModelProperty("类名")
@TableField("class_name")
private String className;
@ApiModelProperty("方法名称")
@TableField("method_name")
private String methodName;
@ApiModelProperty("服务器IP")
@TableField("server_ip")
private String serverIp;
@ApiModelProperty("线程名称")
@TableField("thread_name")
private String threadName;
@ApiModelProperty("创建时间")
@TableField("create_time")
private LocalDateTime createTime;
@ApiModelProperty("过期时间")
@TableField("expire_time")
private LocalDateTime expireTime;
@ApiModelProperty("备注")
@TableField("remark")
private String remark;
}
- 创建应用锁的加锁和解锁方法
- 实现代码
package com.ht.atp.plat.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ht.atp.plat.entity.AppLock; import com.ht.atp.plat.mapper.AppLockMapper; import com.ht.atp.plat.service.AppLockService; import org.springframework.stereotype.Service; @Service public class AppLockServiceImpl extends ServiceImpl<AppLockMapper, AppLock> implements AppLockService { @Override public void lock(AppLock appLock) { try { this.baseMapper.insert(appLock); } catch (Exception ex) { // 获取锁失败,则重试 try { Thread.sleep(50); this.lock(appLock); } catch (InterruptedException e) { e.printStackTrace(); } } } @Override public void unlock(AppLock appLock) { this.baseMapper.deleteById(appLock.getId()); } }
- 通过自旋的方式加锁
- 解锁直接将锁删除
- 实现扣减库存的业务代码
- 实现代码
@Autowired private AppLockService appLockService; @Override public void checkAndReduceStock() { AppLock appLock = new AppLock(); appLock.setLockName("lock"); appLock.setClassName(this.getClass().getName()); appLock.setMethodName("checkAndReduceStock"); appLock.setServerIp("127.0.0.1"); appLock.setThreadName(Thread.currentThread().getName()); appLock.setCreateTime(LocalDateTime.now()); appLock.setExpireTime( LocalDateTime.now().plusSeconds(30)); appLock.setRemark("这是一个测试用锁!"); try { // 加锁 appLockService.lock(appLock); // 查询库存 WmsStock wmsStock = baseMapper.selectById(1L); // 验证库存大于0再扣减库存 if (wmsStock != null && wmsStock.getStockQuantity() > 0) { wmsStock.setStockQuantity(wmsStock.getStockQuantity() - 1); baseMapper.updateById(wmsStock); } } catch (Exception e) { e.printStackTrace(); } finally { // 解锁 appLockService.unlock(appLock); } }
- 启动7000、7001、7002服务器,并将库存设置为10000
- 启动jmeter压测扣减库存接口
- 最终压测结果
- 库存扣减为:0
- 压测结果:平均响应时间3461ms,每秒钟的吞吐量为27,性能相比redis和zookeeper锁,性能相差不少。
- 关于mysql分布式锁的几点总结
- mysql数据库存在单点故障,一旦数据库宕机,将导致应用锁不可用,可通过搭建主备数据库保证高可用
- 没有实现自动释放锁:可通过定时任务扫描应用锁数据,如果过期时间小于当前时间,将锁删除掉;或者通过消息中间件去删除锁
- 锁是非重入的:可以通过使用主机信息、端口号、线程号等信息判断是否是同一个线程,如果是同一个线程,则锁可重入
- mysql数据库分布式锁性能一般,不及redis和zookeeper分布式锁,使用简单
结语
关于使用mysql分布式锁解决“超卖”问题的内容到这里就结束了,后会有期。。。。。。