首先说结论,可以做,但不推荐做。 我们并不推荐使用数据库实现分布式锁。
如果非要这么做,实现大概有两种。
1、锁住Java的方法,借助insert实现
如何用数据库实现分布式锁呢,简单来说就是创建一张锁表,比如:
CREATE TABLE `methodLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
`desc` varchar(1024) NOT NULL DEFAULT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
当我们想锁住某个方法的时候,就往这张表插入一条数据,因为method_name
建立了唯一索引,其他方法要执行的时候,就会因为插入失败而告终,最终保证只能是某一个服务的某一个线程成功执行该方法。执行完毕后,再delete
这条数据就行了。
优点:容易理解,使用简单。
缺点:数据库有性能问题。
2、锁住某一行,借助for update实现
这个其实就是借助MySQL本身的行级锁排他锁来实现,但是要注意,where条件里面必须要走索引,防止退化为表级锁。
select查询语句是不会加锁的,但是select .......for update除了有查询的作用外,还会加锁,那么select......for update会锁表还是锁行?
验证:
创建表student,id是自增主键,给age 创建普通索引,phone创建唯一索引,字段name不加索引.
CREATE TABLE `student` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`age` int NOT NULL,
`phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `index_phone` (`phone`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4;
插入模拟数据:
INSERT INTO `student` (`id`, `name`, `age`, `phone`)
VALUES
(1, '22222', 1, '111'),
(2, '英语', 2, '1212'),
(3, '22222', 3, '2121'),
(4, '22222', 4, '21212'),
(5, '1111111', 5, '4444');
用到的索引相关命令
#查询表student的所有索引
show index from student
#id是自增主键
#age 创建普通索引
CREATE INDEX idx_age ON student (`age`);
#phone创建唯一索引
CREATE UNIQUE INDEX index_phone ON student(phone);
#name是普通字段
#age 创建普通索引,
#phone创建唯一索引
查看当前的索引情况:
需要关闭自动提交,通过set @@autocommit=0; 设置为手动提交。0代表手动提交,1代表自动提交。
实例1(主键): 会话1查询id=1的数据加了for update悲观锁,开启事务,并且没有提交。
会话2去更新id为1的数据,被阻塞了
当会话1,提交事务(释放锁),会话2立刻就会查询到数据。
证明:根据主键进行 for update 查询时是行锁
实例2(普通索引age):
给普通索引age添加悲观锁,会话1查询age=5的记录,开启事务并不提交。在会话2中更新age=5,发现sql阻塞。
而在会话2中更新age=3的记录就不会阻塞。
证明:根据普通索引进行 for update 查询时是行锁 实例3:(唯一索引) 给唯一索引phone添加悲观锁,会话1查询phone=1212的记录,开启事务并不提交。在会话2中更新phone=1212的记录,发现sql阻塞。
而在会话2中更新phone=111的记录就不会阻塞。
证明:根据唯一索引进行 for update 查询时是行锁
实例4:普通字段name
给普通字段name 添加悲观锁,会话1查询name=‘英语’的记录,开启事务并不提交。在会话2中更新name=‘英语’的记录,发现sql阻塞。
在会话2中更新name=‘22222’的记录,发现sql同样会阻塞。
证明:根据普通字段进行 for update 查询时是表锁
结论: 如果查询条件用了索引(普通索引,唯一索引)/主键,那么select ..... for update就会进行行锁。如果是普通字段,那么select ..... for update就会进行锁表。
实战
如何在java代码中使用悲观锁?
例如,小明和小红同时购买了同一件商品,系统会生成两个订单。在执行减少库存的操作之前,系统会对该商品的库存进行加锁。这意味着只有一个用户能够成功地执行减少库存的操作,而另一个用户必须等待第一个用户完成操作并释放锁后才能执行。 通过使用悲观锁,我们可以有效避免多个用户同时购买同一件商品导致的库存冲突问题。悲观锁确保了数据的一致性,但也带来了一些性能上的损耗,因为其他用户必须等待锁的释放才能继续操作。
实现方式:
@Select("select * from tb_goods where goods_id = #{goodsId} for update")
public TbGoods lock(String goodsId);
业务代码:
@Service
public class OrderService {
@Autowired
private TbGoodsMapper goodsMapper;
@Autowired
private TbOrderMapper orderMapper;
@Transactional
public void buy(String goodsId){
//获取数据库悲观锁(行锁)
goodsMapper.lock(goodsId);
//1:查询商品信息
TbGoods tbGoods = goodsMapper.selectById(goodsId);
if (tbGoods == null) {
return;
}
//2:判断库存
if(tbGoods.getGoodsStock1() <= 1){
return;
}
//3:下单
TbOrder tbOrder = new TbOrder();
tbOrder.setOrderId(UUID.randomUUID().toString());
tbOrder.setGoodsId(Integer.parseInt(goodsId));
tbOrder.setOrderAmount(tbGoods.getGoodsPrice());
orderMapper.insert(tbOrder);
//4:修改库存
tbGoods.setGoodsStock1(tbGoods.getGoodsStock1() - 1);
goodsMapper.updateById(tbGoods);
}
}