应用场景
- 超卖现象
- 解决办法
- update的行锁
- Synchronized关键字,且手动控制事务
- 使用ReentrantLock,jdk1.5之后出现
- 解决办法
单体下锁的局限性
- 无法跨JVM进程,跨多应用环境
基于数据库悲观锁的分布式锁
实现步骤
- 多个进程、多个线程访问共同组建数据库
- 通过 select…for update访问同一条数据
- for update锁定数据,其他线程只能等待
注意:需要关闭事务的自动提交。
优缺点
- 优点:简单方便、便于理解、抑郁操作
- 缺点:并发量大时,对数据库压力较大
- 建议:所谓锁的数据库与业务数据库分开
代码实现
-
从数据库查询锁
@Select("select * from `lock` where business_code='demo' for update") Lock getLock();
-
获取分布式锁,执行方法,未获得锁,会阻塞,等待锁被释放,然后再获得锁执行方法
@RestController @Slf4j public class SqlConreoller { @Autowired private SqlMapper sqlMapper; @GetMapping("sqllock") @Transactional(rollbackFor = Exception.class) public String getLock() throws Exception { log.info("进入了方法"); Lock lock = sqlMapper.getLock(); if(null==lock) throw new Exception("获取不到分布式锁"); log.info("进入了锁"); try { Thread.sleep(20000); } catch (InterruptedException e) { e.printStackTrace(); } return "getlock"; } }
基于Redis的Setnx实现分布式锁
实现原理
- 获取锁的redis命令
- set resource_name my_random_value NX PX 30000
- resource_name :资源名称
- my_random_value :随机值,每个线程的随机值都不相同,用于释放锁时的校验
- NX :key不存在时设置成功,key存在则设置不成功
- PX :自动失效时间,出现异常情况,锁可以过期失效
- 主要是利用NX的原子性,多个线程并发的时候,只有一个线程设置成功
- 只有获取到锁的线程,才能执行业务
- 锁还有过期时间,若发生异常,锁到达过期时间会自动失效,其他线程可以继续获取锁
- 释放锁才有redis的DELETE命令
- 释放锁是校验之前设置随机数,相同才能释放
- 释放锁使用LUA脚本
代码实现
-
分布式锁工具类
@Slf4j public class RedisLock implements AutoCloseable { private RedisTemplate redisTemplate; private String key; private String value; //单位:秒 private int expireTime; public RedisLock(RedisTemplate redisTemplate, String key, int expireTime) { this.redisTemplate = redisTemplate; this.key = key; this.value = UUID.randomUUID().toString(); this.expireTime = expireTime; } /** * 获取分布式锁 * * @return */ public boolean getLock() { RedisCallback<Boolean> redisCallback = connection -> { // 设置NX RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent(); // 设置过期时间 Expiration expiration = Expiration.seconds(expireTime); // 序列化key byte[] redisKey = redisTemplate.getKeySerializer().serialize(key); // 序列化value byte[] redisValue = redisTemplate.getValueSerializer().serialize(value); // 执行setnx操作 Boolean result = connection.set(redisKey, redisValue, expiration, setOption); return result; }; Boolean lock = (Boolean) redisTemplate.execute(redisCallback); return lock; } /** * 释放分布式锁 * * @return */ public boolean unLock() { // LUA脚本 String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" + " return redis.call(\"del\",KEYS[1])\n" + "else\n" + " return 0\n" + "end"; RedisScript<Boolean> redisScript=RedisScript.of(script,Boolean.class); List<String> keys = Arrays.asList(key); Boolean result =(Boolean) redisTemplate.execute(redisScript, keys, value); log.info("释放锁结果:"+result); return result; } @Override public void close() throws Exception { unLock(); } }
-
测试分布式锁,未获得锁的线程会直接结束,不会阻塞
@Slf4j @RestController public class RedisController { @Autowired private RedisTemplate redisTemplate; @GetMapping("redisLock") public String getLock() { String redisKey = "rediskey"; log.info("进入了方法"); try (RedisLock redisLock = new RedisLock(redisTemplate, redisKey, 30)) { boolean lock = redisLock.getLock(); if(lock){ log.info("进入了锁"); Thread.sleep(15000); } } catch (Exception e) { e.printStackTrace(); } log.info("方法执行完成"); return "redislock"; } }
基于Zookeeper的瞬时节点实现分布式锁
Zookeeper数据结构
- 持久节点
- 瞬时节点:有序,瞬时节点不可再有子节点,会话结束后瞬时节点会自动消失
Zookeeper的观察器
- 可设置观察器的3个方法:getData();getChildren();exists();
- 节点数据发生变化,发送给客户端
- 观察器只能监控一次,再次监控需要重新设置
实现原理
- 利用Zookeeper瞬时节点的特性
- 多线程并发创建瞬时节点,得到有序数列
- 序号最小的节点得到锁
- 其他线程则监听自己序号的前一个序号
- 前一个执行完,删除自己序号的节点
- 下一个序号节点得到通知,继续执行
- 以此类推,根据之前确认好的顺序执行
代码实现
-
Zookeeper分布式锁的工具类
@Slf4j public class ZookeeperLock implements AutoCloseable, Watcher { private ZooKeeper zooKeeper; private String znode; public ZookeeperLock() throws IOException { this.zooKeeper = new ZooKeeper("localhost:2181", 10000, this); } /** * 获取锁 * @param businessCode * @return */ public boolean getLock(String businessCode) { try { //创建业务 根节点 Stat stat = zooKeeper.exists("/" + businessCode, false); if (stat == null) { zooKeeper.create("/" + businessCode, businessCode.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } //创建瞬时有序节点 /order/order_00000001 znode = zooKeeper.create("/" + businessCode + "/" + businessCode + "_", businessCode.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); //获取业务节点下 所有的子节点 List<String> childrenNodes = zooKeeper.getChildren("/" + businessCode, false); //子节点排序 Collections.sort(childrenNodes); //获取序号最小的(第一个)子节点 String firstNode = childrenNodes.get(0); //如果创建的节点是第一个子节点,则获得锁 if (znode.endsWith(firstNode)) { return true; } //不是第一个子节点,则监听前一个节点 String lastNode = firstNode; for (String node : childrenNodes) { if (znode.endsWith(node)) { zooKeeper.exists("/" + businessCode + "/" + lastNode, true); break; } else { lastNode = node; } } synchronized (this) { wait(); } return true; } catch (Exception e) { e.printStackTrace(); } return false; } /** * 释放锁 * @throws Exception */ @Override public void close() throws Exception { zooKeeper.delete(znode, -1); zooKeeper.close(); log.info("我已经释放了锁!"); } /** * 通知下一节点 * @param event */ @Override public void process(WatchedEvent event) { if (event.getType() == Event.EventType.NodeDeleted) { synchronized (this) { notify(); } } } }
-
使用分布式锁,未获得锁,会阻塞,等待锁被释放,然后再获得锁执行方法
@Slf4j @RestController public class ZookeeperController { @GetMapping("zookeeperLock") public String getLock() { log.info("进入方法"); try(ZookeeperLock zookeeperLock=new ZookeeperLock()) { boolean lock = zookeeperLock.getLock("zookeeper"); if(lock){ log.info("进入了锁"); Thread.sleep(10000); } }catch (Exception e){ e.printStackTrace(); } log.info("方法执行完成"); return "zookeeperLock"; } }
基于Zookeeper的Curator客户端实现分布式锁
- curator客户端已经实现了分布式锁
- 直接调用即可
代码实现
@Slf4j
@RestController
public class CuratorController {
@GetMapping("curatorLock")
public String getLock() {
log.info("进入了方法");
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181", retryPolicy);
client.start();
InterProcessMutex lock=new InterProcessMutex(client,"/order");
try {
if(lock.acquire(30, TimeUnit.SECONDS)){
try {
log.info("进入了锁");
Thread.sleep(10000);
}finally {
lock.release();
log.info("释放了锁");
}
}
} catch (Exception e) {
e.printStackTrace();
}
return "curatorLock";
}
}
基于Redisson实现分布式锁
- Redisson客户端已经实现了分布式锁
- 直接调用即可
代码实现
-
获取Redisson客户端
@Configuration public class RedissonConfig { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private String port; @Bean public RedissonClient getRedisson(){ Config config = new Config(); config.useSingleServer().setAddress("redis://" + host + ":" + port); //添加主从配置 // config.useMasterSlaveServers().setMasterAddress("").setPassword("").addSlaveAddress(new String[]{"",""}); return Redisson.create(config); } }
-
使用分布式锁,未获得锁,会阻塞,等待锁被释放,然后再获得锁执行方法
@RestController @Slf4j public class RedissonController { @Autowired private RedissonClient redisson; @GetMapping("redissonLock") public String getLock(){ log.info("进入方法"); RLock rLock = redisson.getLock("redissonLock"); try { rLock.lock(30, TimeUnit.SECONDS); log.info("进入了锁"); Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("方法执行完成"); return "redissonLock"; } }
分布式锁实现方案优缺点分析
方式 | 优点 | 缺点 |
---|---|---|
数据库 | 实现简单、便于理解 | 对数据库压力大 |
Redis | 易于理解 | 自己实现、不支持阻塞 |
Zookeeper | 支持阻塞 | 需理解Zookeeper、程序复杂 |
Curator | 提供锁的方法 | 依赖Zookeeper,强一致 |
Redisson | 提供锁的方法,可阻塞 |
总结
- 在项目中不推荐自己编写的分布式锁,平时可以自己编写,提高动手能力
- 推荐Redisson和Curator实现分布式锁