在项目中经常会遇到并发安全问题, 这时我们可以使用锁来进行线程同步. 于是我们可以根据具体的情况使用 synchronized 关键字来修饰方法或者代码块. 也可以使用 java 5 以后的 Lock 来实现, 与 synchronized 关键字相比, Lock 的使用更灵活, 可以有加锁超时时间, 公平性等优势. 但是 synchronized 关键字和 Lock 作用范围也只是当前应用, 如果分布式部署, 那无法保证某个数据在同时间只有一个线程访问, 这时我们可以考虑使用中间层
接下来简单介绍本人开源的一个分布式锁的用法
一. 导入依赖
- <dependency>
- <groupId>cn.gjing</groupId>
- <artifactId>tools-Redis</artifactId>
- <version>1.0.0</version>
- </dependency>
二. 启动类标注注解
- /**
- * @author Gjing
- */
- @SpringBootApplication
- @EnableRedisLock
- public class TestRedisApplication {
- public static void main(String[] args) {
- SpringApplication.run(TestRedisApplication.class, args);
- }
- }
三. 具体使用
参数信息
key: 锁对应的 key
value: 随机字符串
expire: 锁过期时间, 单位秒
timeout: 获取锁超时时间, 单位毫秒
retry: 重新获取锁间隔时间, 单位毫秒
1. 注解方式
- @Lock(String key, int expire , int timeout, int retry)
- /**
- * @author Gjing
- **/
- @RestController
- public class TestController {
- private static int num = 20;
- @GetMapping("/test1")
- @Lock(key = "test1")
- public void test1() {
- System.out.println("当前线程:" + Thread.currentThread().getName());
- if (num == 0) {
- System.out.println("卖完了");
- return;
- }
- num--;
- System.out.println("还剩余:" + num);
- }
- }
ab 压测执行结果
2. 手动控制方式
注入 AbstractLock 依赖
- @Resource
- private AbstractLock abstractLock;
在要锁住的地方加入 abstractLock.lock(), 获取锁成功返回一个解锁的值, 失败返回 null
String lock(String key, String value, int expire, int timeout, int retry)
需要释放的地方使用 abstractLock.release(), 释放锁成功返回当前被解锁的 key, 失败返回 null
String release(String key, String value)
使用案例
- /**
- * @author Gjing
- **/
- @RestController
- public class LockController {
- @Resource
- private AbstractLock abstractLock;
- private static int num = 10;
- @GetMapping("/test2")
- public void test2() {
- String lock = null;
- try {
- lock = this.abstractLock.lock("testLock", RandomUtil.generateMixString(5), 20, 10000, 50);
- System.out.println("当前线程:" + Thread.currentThread().getName());
- if (num == 0) {
- System.out.println("卖完了");
- return;
- }
- num--;
- System.out.println("还剩余:" + num);
- } finally {
- this.abstractLock.release("testLock", lock);
- }
- }
- }
ab 压测结果
注意!!!
锁对应的 key 最好唯一, 否则会造成多个方法在同时共享一个锁, 造成不好的结果, 解锁时传入的 value, 一定要是获取锁得到的 value, 否则会解锁失败, 避免造成解锁其他人的锁
3. 重写异常处理
某个请求获取锁超时后, 默认会返回超时异常信息, 如果要自定义返回, 可以继承 AbstractLockTimeoutHandler 超时异常处理类
- /**
- * @author Gjing
- **/
- @Component
- public class LockExceptionHandler extends AbstractLockTimeoutHandler {
- @Override
- public ResponseEntity timeoutAfter(TimeoutException e) {
- // TODO: 实现自定义处理的逻辑
- }
- }
4. 自定义实现锁
本项目使用 Redis 和 lua 脚本结合使用实现锁, 如若想使用自己的锁, 可以继承 AbstartetLock 类
- /**
- * @author Gjing
- **/
- public class DemoLock extends AbstractLock {
- @Override
- public String lock(String s, String s1, int i, int i1, int i2) {
- return null;
- }
- @Override
- public String release(String s, String s1) {
- return null;
- }
- }
四. 使用建议
该锁建议使用单独的单机 Redis, 如果是在 Redis sentinel 集群中情况就有所不同在 Redis sentinel 集群中, 我们具有多台 Redis, 他们之间有着主从的关系, 例如一主二从. 我们的 set 命令对应的数据写到主库, 然后同步到从库. 当我们申请一个锁的时候, 对应就是一条命令 setnx mykey myvalue , 在 Redis sentinel 集群中, 这条命令先是落到了主库. 假设这时主库 down 了, 而这条数据还没来得及同步到从库, sentinel 将从库中的一台选举为主库了. 这时, 我们的新主库中并没有 mykey 这条数据, 若此时另外一个 client 执行 setnx mykey hisvalue , 也会成功, 即也能得到锁. 这就意味着, 此时有两个 client 获得了锁
使用中有任何问题以及 BUG, 欢迎评论留言, 我会及时回复和更新
来源: https://yq.aliyun.com/articles/706641