2. 基于mysql实现分布式锁
不管是jvm锁还是mysql锁,为了保证线程的并发安全,都提供了悲观独占排他锁。所以独占排他也是 分布式锁的基本要求。 可以利用唯一键索引不能重复插入的特点实现。设计表如下:
1. CREATE TABLE `db_lock` ( 2. `id` bigint(20) NOT NULL AUTO_INCREMENT, 3. `lock_name` varchar(50) NOT NULL COMMENT '锁名', 4. `class_name` varchar(100) DEFAULT NULL COMMENT '类名', 5. `method_name` varchar(50) DEFAULT NULL COMMENT '方法名', 6. `server_name` varchar(50) DEFAULT NULL COMMENT '服务器ip', 7. `thread_name` varchar(50) DEFAULT NULL COMMENT '线程名', 8. `create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP 9. 10. COMMENT '获取锁时间', 11. `desc` varchar(100) DEFAULT NULL COMMENT '描述', 12. PRIMARY KEY (`id`), 13. UNIQUE KEY `idx_unique` (`lock_name`) 14. ) ENGINE=InnoDB AUTO_INCREMENT=1332899824461455363 DEFAULT CHARSET=utf8;
Lock实体类:
1. @Data 2. @AllArgsConstructor 3. @NoArgsConstructor 4. @TableName("db_lock") 5. 6. public class Lock { 7. private Long id; 8. private String lockName; 9. private String className; 10. private String methodName; 11. private String serverName; 12. private String threadName; 13. private Date createTime; 14. private String desc; 15. }
LockMapper接口:
1. public interface LockMapper extends BaseMapper<Lock> { 2. }
2.1. 基本思路
synchronized关键字和ReetrantLock锁都是独占排他锁,即多个线程争抢一个资源时,同一时刻只有 一个线程可以抢占该资源,其他线程只能阻塞等待,直到占有资源的线程释放该资源。
- 线程同时获取锁(insert)
- 获取成功,执行业务逻辑,执行完成释放锁(delete)
- 其他线程等待重试
2.2. 代码实现
改造StockService:
1. @Service 2. 3. public class StockService { 4. @Autowired 5. private StockMapper stockMapper; 6. @Autowired 7. private LockMapper lockMapper; 8. /** 9. * 数据库分布式锁 10. */ 11. public void checkAndLock() { 12. // 加锁 13. Lock lock = new Lock(null, "lock", this.getClass().getName(), new 14. Date(), null); 15. try { 16. this.lockMapper.insert(lock); 17. } catch (Exception ex) { 18. // 获取锁失败,则重试 19. try { 20. Thread.sleep(50); 21. this.checkAndLock(); 22. } catch (InterruptedException e) { 23. e.printStackTrace(); 24. } 25. } 26. // 先查询库存是否充足 27. Stock stock = this.stockMapper.selectById(1L); 28. // 再减库存 29. if (stock != null && stock.getCount() > 0){ 30. stock.setCount(stock.getCount() - 1); 31. this.stockMapper.updateById(stock); 32. } 33. // 释放锁 34. this.lockMapper.deleteById(lock.getId()); 35. } 36. }
加锁:
1. // 加锁 2. Lock lock = new Lock(null, "lock", this.getClass().getName(), new Date(), null); 3. try { 4. this.lockMapper.insert(lock); 5. } catch (Exception ex) { 6. // 获取锁失败,则重试 7. try { 8. Thread.sleep(50); 9. this.checkAndLock(); 10. } catch (InterruptedException e) { 11. e.printStackTrace(); 12. } 13. }
解锁:
1. // 释放锁 2. this.lockMapper.deleteById(lock.getId());
使用Jmeter压力测试结果:
可以看到性能感人。mysql数据库库存余量为0,可以保证线程安全。
2.3. 缺陷及解决方案
1. 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
解决方案:给锁数据库 搭建主备
2. 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
解决方案:只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
3. 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
解决方案:记录获取锁的主机信息和线程信息,如果相同线程要获取锁,直接重入。
4. 受制于数据库性能,并发能力有限。
解决方案:无法解决。