001/**
002 * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com).
003 * <p>
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * http://www.apache.org/licenses/LICENSE-2.0
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.jboot.support.redis;
017
018import io.jboot.Jboot;
019import io.jboot.utils.QuietlyUtil;
020
021/**
022 * Created by michael.
023 * <p>
024 * Redis 分布式锁
025 * <p>
026 * 使用方法:
027 * <p>
028 * JbootRedisLock lock = new JbootRedisLock("lockName");
029 * try{
030 * boolean acquire = lock.acquire();
031 * if(acquire){
032 * // do your something
033 * }
034 * }finally {
035 * lock.release();
036 * }
037 * <p>
038 * 使用方法2:
039 * JbootRedisLock lock = new JbootRedisLock("lockName");
040 * lock.runIfAcquired(new Runnable(){
041 * <p>
042 * public void run() {
043 * //do your something
044 * }
045 * });
046 */
047public class JbootRedisLock {
048
049    // 60 秒 expireMsecs 锁持有超时,防止线程在入锁以后,无限的执行下去,让锁无法释放
050    long expireMsecs = 1000 * 60;
051
052    // 锁等待超时
053    long timeoutMsecs = 0;
054
055    private final String lockName;
056    private boolean locked = false;
057    private JbootRedis redis;
058
059    /**
060     * 创建redis分布式锁
061     *
062     * @param lockName 锁的名称
063     */
064    public JbootRedisLock(String lockName) {
065        if (lockName == null) {
066            throw new NullPointerException("lockName must not be null.");
067        }
068        this.lockName = lockName;
069        this.redis = Jboot.getRedis();
070    }
071
072    /**
073     * 创建redis分布式锁
074     *
075     * @param lockName     锁名称
076     * @param timeoutMsecs 获取锁的时候,等待时长
077     */
078    public JbootRedisLock(String lockName, long timeoutMsecs) {
079        if (lockName == null) {
080            throw new NullPointerException("lockName must not be null.");
081        }
082        this.lockName = lockName;
083        this.timeoutMsecs = timeoutMsecs;
084        this.redis = Jboot.getRedis();
085    }
086
087
088    /**
089     * @param lockName     锁名称
090     * @param timeoutMsecs 获取锁的时候,等待时长
091     * @param expireMsecs  超时时长
092     */
093    public JbootRedisLock(String lockName, long timeoutMsecs, long expireMsecs) {
094        if (lockName == null) {
095            throw new NullPointerException("lockName must not be null.");
096        }
097        this.lockName = lockName;
098        this.timeoutMsecs = timeoutMsecs;
099        this.expireMsecs = expireMsecs;
100
101        this.redis = Jboot.getRedis();
102    }
103
104
105    public long getTimeoutMsecs() {
106        return timeoutMsecs;
107    }
108
109    public void setTimeoutMsecs(long timeoutMsecs) {
110        this.timeoutMsecs = timeoutMsecs;
111    }
112
113    public long getExpireMsecs() {
114        return expireMsecs;
115    }
116
117    public void setExpireMsecs(long expireMsecs) {
118        this.expireMsecs = expireMsecs;
119    }
120
121    public void runIfAcquired(Runnable runnable) {
122        if (runnable == null) {
123            throw new NullPointerException("runnable must not null!");
124        }
125        try {
126            if (acquire()) {
127                runnable.run();
128            }
129        } finally {
130            //执行完毕,释放锁
131            release();
132        }
133    }
134
135
136    /**
137     * 获取锁
138     *
139     * @return true:活动锁了 , false :没获得锁。 如果设置了timeoutMsecs,那么这个方法可能被延迟 timeoutMsecs 毫秒。
140     */
141    public boolean acquire() {
142        long timeout = timeoutMsecs;
143        do {
144            try {
145                //超时时间
146                long expiredTime = System.currentTimeMillis() + expireMsecs + 1;
147
148                Long result = redis.setnx(lockName, expiredTime);
149
150                if (result == null) {
151                    continue;
152                }
153
154                // lock acquired
155                if (result == 1) {
156                    locked = true;
157                    return true;
158                }
159
160                //之前的保存时间
161                Long savedValue = redis.get(lockName);
162
163                //这个锁已经过期了,此时可以去设置新的key
164                if (savedValue != null && savedValue < System.currentTimeMillis()) {
165
166
167                    //获取上一个锁到期时间,并设置现在的锁到期时间,
168                    //只有一获个线程才能取上一个线上的设置时间,因为jedis.getSet是同步的
169                    Long oldValue = redis.getSet(lockName, expiredTime);
170
171
172                    if (oldValue != null && oldValue.equals(savedValue)) {
173                        //如果这个时候,多个线程恰好都到了这里
174                        //只有一个线程的设置值和当前值相同,他才有权利获取锁
175                        //lock acquired
176                        locked = true;
177                        return true;
178                    }
179                }
180            } finally {
181                if (timeout > 0) {
182                    timeout -= 100;
183                    QuietlyUtil.sleepQuietly(100);
184                }
185            }
186        } while (timeout > 0);
187
188        return false;
189    }
190
191
192    /**
193     * 是否获得 锁 了
194     *
195     * @return
196     */
197    public boolean isLocked() {
198        return locked;
199    }
200
201    /**
202     * 释放 锁
203     */
204    public void release() {
205        if (!isLocked()) {
206            return;
207        }
208        if (Jboot.getRedis().del(lockName) > 0) {
209            locked = false;
210        }
211    }
212}