为了防止并发编程,或多或少会接触到锁这个概念。Java的锁有 synchronized 和 Lock 两种。
最大的区别在于:synchronized是个关键字,而Lock是个接口类。但是Java的锁,只能保证在同一个JVM中执行的时候起作用,那么分布式项目呢?
一、什么是锁
锁的执行:
· 加锁(锁持有)
去KTV开了个包间,把门一关,这个时候你就给这个包间加了个锁,明确说明这个包间是本大爷的了。其他人想用这个包间,就只能等你用完了。这个时候你持有了锁。
· 解锁(锁释放)
唱着唱着你不想唱了,把包间一退,这个时候别人就能使用这个包间了,你就把这个锁主动给释放掉了。
· 锁超时:
唱嗨了怎么办?忘了时间忘了我。拿着锁不释放。这个时候包间会强制终止包间里的播放器,告诉你超时了,该走了。这个时候也会释放掉你的锁,但是这种释放是被动的。
二、分布式锁
思想:分布式锁是一种思想,照顾要
分类:分布式锁的实现有很多,比如基于数据库、memcached、Redis、系统文件、zookeeper等。
三、为什么要用分布式锁
为了解决在不同JVM中,线程并发执行的问题。
如:我有一个定时任务,在机器A、B、C上都有,如果我不加分布式锁,那么它会执行3次。如果这个定时任务是发微信模板推送的,那么用户会接收到3次推送,就很烦,可能接到大量投诉,饭碗除脱。
四、各种分布式锁实现介绍
4.1 数据库
也不推荐用,不如单线程的redis来得痛快。
1.建表
DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '方法名',
`expiration_time` datetime(0) NOT NULL COMMENT '过期时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uidx_method_name`(`method_name`) USING BTREE COMMENT '方法名唯一'
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '方法锁表' ROW_FORMAT = Dynamic;
2.想要执行某个方法,就使用这个方法名向表中插入数据;
3.因为对方法名建了唯一索引,所以只能插入一条。插入成功的算锁定成功。插入失败的,未获取到锁。
4.方法执行结束,执行删除该方法名的sql
5.锁失效机制,定时任务检测数据库给过期的数据执行删除操作。(定时任务可能也会出现临界值的相关情况)
6.因为是基于数据库的操作,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换。
4.2 memcached
有点LOW
4.3 Redis
4.3.1 不可重入锁
1.引包
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
2.注入类
public static JedisCluster jedisCluster = SpringUtils.getBean(JedisCluster.class);
3.加锁解锁方法
package com.nanci.utils;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
/**
*
* redis实现分布式锁,并释放锁
*/
public class RedisUtils {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final Long RELEASE_SUCCESS = 1L;
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间,毫秒
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(JedisCluster jedis, String lockKey, String requestId, Long expireTime) {
/*
*设置锁并设置超时时间,lockKey表示Redis key,requestId表示Redis value,SET_IF_NOT_EXIST表示有值不进行设置(NX),
* SET_WITH_EXPIRE_TIME表示是否设置超时时间(PX)设置,expireTime表示设置超时的毫秒值
* */
// jedis---3.1.0的版本
String result = jedis.set(lockKey, requestId, new SetParams().px(expireTime));
// jedis---2.9.0的版本
//String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(JedisCluster jedis, String lockKey, String requestId) {
/*
* 利用Lua脚本代码,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)
* eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令,这样就不会出现上一个代码执行完挂了后边的出现问题,还是一致性的解决
* */
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
4.3.2 可重入锁
redisson
4.4 zookeeper
一般用不上