mongo分布式锁Java实现
一、分布式锁使用场景:
代码部署在多台服务器上,即分布式部署。
多个进程同步访问一个共享资源。
二、需要的技术:
数据库:mongo
java:mongo操作插件类 MongoTemplate(maven引用),如下:
1 <!--mongodo开始--> 2 <dependency> 3 <groupId>org.springframework.data</groupId> 4 <artifactId>spring-data-mongodb</artifactId> 5 <version>1.8.2.RELEASE</version> 6 </dependency> 7 <dependency> 8 <groupId>org.springframework.data</groupId> 9 <artifactId>spring-data-commons</artifactId> 10 <version>1.10.0.RELEASE</version> 11 </dependency> 12 <dependency> 13 <groupId>org.mongodb</groupId> 14 <artifactId>mongo-java-driver</artifactId> 15 <version>2.13.0-rc2</version> 16 </dependency> 17 <!--mongodo结束-->
三、实现代码:
主实现逻辑及外部调用方法,获得锁调用getLock,释放锁调用releaseLock,详情如下:
1 import java.util.HashMap;
2 import java.util.List;
3 import java.util.Map;
4
5
6 public class MongoDistributedLock {
7
8 static MongoLockDao mongoLockDao;
9
10 static {
11 mongoLockDao = SpringBeanUtils.getBean("mongoLockDao");
12 }
13
14 /**
15 * 获得锁的步骤:
16 * 1、首先判断锁是否被其他请求获得;如果没被其他请求获得则往下进行;
17 * 2、判断锁资源是否过期,如果过期则释放锁资源;
18 * 3.1、尝试获得锁资源,如果value=1,那么获得锁资源正常;(在当前请求已经获得锁的前提下,
还可能有其他请求尝试去获得锁,此时会导致当前锁的过期时间被延长,由于延长时间在毫秒级,可以忽略。)
19 * 3.2、value>1,则表示当前请求在尝试获取锁资源过程中,其他请求已经获取了锁资源,即当前请求没有获得锁;
20 * !!!注意,不需要锁资源时,及时释放锁资源!!!。
21 *
22 * @param key
23 * @param expire
24 * @return
25 */
26 public static boolean getLock(String key, long expire) {
27 List<MongoLock> mongoLocks = mongoLockDao.getByKey(key);
28 //判断该锁是否被获得,锁已经被其他请求获得,直接返回
29 if (mongoLocks.size() > 0 && mongoLocks.get(0).getExpire() >= System.currentTimeMillis()) {
30 return false;
31 }
32 //释放过期的锁
33 if (mongoLocks.size() > 0 && mongoLocks.get(0).getExpire() < System.currentTimeMillis()) {
34 releaseLockExpire(key, System.currentTimeMillis());
35 }
36 //!!(在高并发前提下)在当前请求已经获得锁的前提下,还可能有其他请求尝试去获得锁,
此时会导致当前锁的过期时间被延长,由于延长时间在毫秒级,可以忽略。
37 Map<String, Object> mapResult = mongoLockDao.incrByWithExpire(key, 1, System.currentTimeMillis() + expire);
38 //如果结果是1,代表当前请求获得锁
39 if ((Integer) mapResult.get("value") == 1) {
40 return true;
41 //如果结果>1,表示当前请求在获取锁的过程中,锁已被其他请求获得。
42 } else if ((Integer) mapResult.get("value") > 1) {
43 return false;
44 }
45 return false;
46 }
47
48 /**
49 * 释放锁
50 *
51 * @param key
52 */
53 public static void releaseLock(String key) {
54 Map<String, Object> condition = new HashMap<>();
55 condition.put("key", key);
56 mongoLockDao.remove(condition);
57 }
58
59 /**
60 * 释放过期锁
61 *
62 * @param key
63 * @param expireTime
64 */
65 private static void releaseLockExpire(String key, long expireTime) {
66 mongoLockDao.removeExpire(key, expireTime);
67 }
68 }MongoLockDao实现代码:
1 import org.springframework.data.mongodb.core.FindAndModifyOptions;
2 import org.springframework.data.mongodb.core.query.Criteria;
3 import org.springframework.data.mongodb.core.query.Query;
4 import org.springframework.data.mongodb.core.query.Update;
5 import org.springframework.stereotype.Repository;
6
7 import java.util.HashMap;
8 import java.util.List;
9 import java.util.Map;
10
11
12 @Repository
13 public class MongoLockDao <MongoLock> {
14 private Class<?> clz;
15
16 public Class<?> getClz() {
17 if (clz == null) {
18 //获取泛型的Class对象
19 clz = ((Class<?>)
20 (((ParameterizedType) (this.getClass().getGenericSuperclass())).getActualTypeArguments()[0]));
21 }
22 return clz;
23 }
24
25 /**
26 * 返回指定key的数据
27 *
28 * @param key
29 * @return
30 */
31 public List<MongoLock> getByKey(String key) {
32 Query query = new Query();
33 query.addCriteria(Criteria.where("key").is(key));
34 return (List<MongoLock>) mongoTemplate.find(query, getClz());
35 }
36
37
38 /**
39 * 指定key自增increment(原子加),并设置过期时间
40 *
41 * @param key
42 * @param increment
43 * @param expire
44 * @return
45 */
46 public Map<String, Object> incrByWithExpire(String key, double increment, long expire) {
47 //筛选
48 Query query = new Query();
49 query.addCriteria(new Criteria("key").is(key));
50
51 //更新
52 Update update = new Update();
53 update.inc("value", increment);
54 update.set("expire", expire);
55 //可选项
56 FindAndModifyOptions options = FindAndModifyOptions.options();
57 //没有则新增
58 options.upsert(true);
59 //返回更新后的值
60 options.returnNew(true);
61 Map<String, Object> resultMap = new HashMap<>();
62 resultMap.put("value", Double.valueOf(((MongoLock)
63 mongoTemplate.findAndModify(query, update, options, getClz())).getValue()).intValue());
64 resultMap.put("expire", Long.valueOf(((MongoLock)
65 mongoTemplate.findAndModify(query, update, options, getClz())).getExpire()).longValue());
66 return resultMap;
67 }
68
69
70 /**
71 * 根据value删除过期的内容
72 *
73 * @param key
74 * @param expireTime
75 */
76 public void removeExpire(String key, long expireTime) {
77 Query query = new Query();
78 query.addCriteria(Criteria.where("key").is(key));
79 query.addCriteria(Criteria.where("expire").lt(expireTime));
80 mongoTemplate.remove(query, getClz());
81 }
82
83 public void remove(Map<String, Object> condition) {
84 Query query = new Query();
85 Set<Map.Entry<String, Object>> set = condition.entrySet();
86 int flag = 0;
87 for (Map.Entry<String, Object> entry : set) {
88 query.addCriteria(Criteria.where(entry.getKey()).is(entry.getValue()));
89 flag = flag + 1;
90 }
91 if (flag == 0) {
92 query = null;
93 }
94 mongoTemplate.remove(query, getClz());
95 }
96
97 }MongoLock实体:
1 public class MongoLock {
2
3 private String key;
4 private double value;
5 private long expire;
6
7 public double getValue() {
8 return value;
9 }
10
11 public void setValue(double value) {
12 this.value = value;
13 }
14
15 public long getExpire() {
16 return expire;
17 }
18
19 public void setExpire(long expire) {
20 this.expire = expire;
21 }
22
23 public String getKey() {
24 return key;
25 }
26
27 public void setKey(String key) {
28 this.key = key;
29 }
30 }四、设计思路
前提:利用mongo实现id自增,且自增过程为原子操作,即线程安全。
假设有A、B两个请求同时请求资源。
当A请求到资源是调用mongo自增 +1,并将结果返回给A,即1。此时结果等于1则表明,A请求过程中没有其他请求请求到资源,将锁资源分配给A。
当B请求到资源是调用mongo自增 +1,并将结果返回给B,即2。此时结果大于1则表明,B请求过程中有其他请求请求到资源,锁资源不能分配给B。
这样就是实现了多个请求请求同一个锁并且排队。
关于锁过期时间 :
如果图中代码1releaseLockExpire(key, System.currentTimeMillis())修改为releaseLockExpire(key),即在释放锁的时候没有传入过期时间,会产生如下情况:
A、B两个请求同时通过条件,进入到代码 1
B执行完删除操作,进入代码2,并且刚刚获得到锁资源,而此时A及有可能刚开始执行释放锁的操作。
此时就会发生,A释放了B刚刚获得的锁,这样B就会失去刚刚获得的锁,而B确没有感知,从而造成逻辑错误。
而releaseLockExpire(key, System.currentTimeMillis()),即在释放锁的时候判断一下过期时间,这样就不会误删B刚刚获得的锁。
