在一些场景中,我们希望一个方法同一时间只被一个线程执行,如果在单机环境下我们可以通过使用Java提供的并发API来限制访问,同一个JVM的线程可以通过共享堆内存中的变量来标记,这个标记其实就是锁,synchronized是通过对象头来标记;Lock接口的实现类是通过一个volatile的int型变量state来实现多线程的可见性和有序性(防止指令被重排序);linux 内核中也是利用互斥量或信号量等内存数据做标记。但是在分布式环境下,就变成了多进程,需要标记在一个所有进程都可见的地方,即一块外部共享内存,比如数据库。除了利用内存数据做锁,实际上任何互斥的都能做锁,如流水表中与时间相关的流水号做幂等校验可以看作是一个不会释放的锁,或者使用某个文件是否存在作为锁等,只需要满足在对标记进行修改能保证原子性和内存可见性即可。
我们对锁的要求一般有以下几点:
1、互斥性:首先要保证一个方法同一时间只被一台呢机器上的一个线程执行
2、防止死锁:任何情况下拿到锁的线程崩溃,没有释放锁,锁要支持能自动释放,防止死锁导致方法不能被执行
3、高可用:如果锁服务挂掉,方法就不会被执行
4、可重入:指同一台机的同一个线程在已经持有锁的条件下调用同一把锁的资源不需要重新获取锁,否则会发生死锁(非必需,按业务要求而定)
5、加锁解锁保证是同一个线程:释放锁的时候必须是同一个线程,避免释放其他线程的锁,多线程交替执行的时候,如果锁的超时时间设置的不合理,第一个线程持有了锁,但是还没等到执行完,锁超时释放了,这时第二个线程就可以加锁成功,开始执行,第一个线程执行结束如果不判断锁的owner就去释放锁,会导致第三个线程也加锁成功,也开始执行,这时就无法保证互斥性了
6、加锁解锁性能:加锁解锁要尽可能快
常见的分布式锁实现有三种:
SET method_name thread_value NX PX TTL
如果是单机版的有单点问题,如果是master-slave架构,如果一个线程加锁成功在数据还未同步到slave时master宕机了,另一台slave成为了新的master,此时主从数据不一致,另一个线程可以从新的master那里加锁成功。还有一个问题就是如果锁的超时时间设置的不合理,第一个线程持有了锁,但是还没等到执行完,锁超时释放了,这时第二个线程就可以加锁成功,开始执行,第一个线程执行完将第二个线程的锁释放,第三个线程也可以获取锁执行,这样无法保证互斥性,所以把握好加锁时间比较重要。
1)基于数据库的表主键唯一性
CREATE TABLE `tb_method_lock_primary_key` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`method_name` varchar(255) DEFAULT NULL,
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`method_desc` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_method` (`method_name`) USING BTREE,
KEY `idx_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
可以对某个方法的名称(可以类名+方法名)执行insert,如果成功即拿到锁;但是这种方式只要insert了如果拿到锁的线程崩溃没有删除这一条记录就会导致死锁发生,还需要启动一个定时任务去删除过期的记录。
2)基于数据库表版本号字段
CREATE TABLE `tb_method_lock_mvcc` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`method_name` varchar(255) DEFAULT NULL,
`method_desc` varchar(255) DEFAULT NULL,
`version` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_method` (`method_name`) USING BTREE,
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
先查询一次 SELECT id , version FROM tb_method_lock_mvcc WHERE id=1;
如果不存在,执行插入 INSERT INTO tb_method_lock_mvcc(method_name,method_desc , version,create_time) VALUES("methodName","methodDesc",1563600714,NOW());
如果存在,执行更新操作 UPDATE tb_method_lock_mvcc SET version = 1563600715 WHERE id=1 AND version = 1563600714;
3)基于数据库排他锁
CREATE TABLE `tb_method_lock_x` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`method_name` varchar(255) DEFAULT NULL,
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`method_desc` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_method` (`method_name`) USING BTREE,
KEY `idx_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
在事务中获取x锁,SELECT id FROM tb_method_lock_x WHERE method_name="methodName" FOR UPDATE;执行完成任务后commit;
4)在业务表中加一个版本号字段
更新的时候仅当期待的版本号与记录当前版本号相同才更新,其实我们业务中经常会用这种,优点就是不需要引入其他中间件,减少不必要的维护成本,只要数据库正常服务就可以进行,缺点是对业务表侵入较大,也不可复用。
优点:简单
缺点:不支持可重入,如果想支持可重入就加一个owner字段,带有与线程相关的一个标识。数据库的最大问题是单点问题,无比保证高可用。而且操作mysql数据库也有一定的开销,性能不靠谱。尤其使用排他锁拿不到锁就会一直阻塞。导致数据库连接越来越多,可能会将数据库连接池资源耗尽。
利用zookeeper先创建一个持久节点,线程获取锁的时候会在该结点下创建临时顺序节点,如果自己是序号最小的结点,就加锁成功,如果不是且想排队继续获取,就向排在它前面的那个结点注册Watcher,如果监听到前一个结点不存在了,就可以获得锁。执行完成任务显示的删除结点,如果异常崩溃,与zookeeper服务器的连接会断开,根据临时结点的特性相关联的结点会自动删除,不会造成死锁。可以直接使用zookeeper第三方库Curutor,这个客户端中封装了一个可重入的锁服务。但是使用zookeeper性能上没有缓存那么高,每次创建、销毁结点都是开销,而且只能由leader来执行,发给follower还需要请求转给leader执行再同步到follower机器上,而且zookeeper客户端与服务器直接的连接需要发送心跳包来探活,遇到网络抖动,如果心跳包丢失,zookeeper服务器会认为客户端挂了,将临时结点删除,其他客户端就可以获取到锁了。这种情况不常见是因为zookeeper有重试机制,一旦zookeeper集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。所以,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。
基于我们的系统本身就是使用mysql和redis,所以为了不增加新的维护成本以及性能方面的考量,我们通常使用mysql和redis。
在没有引入redisson之前我们一般用mysql在业务表中加一个版本号字段或者比较原来的值来保证同时只有一个线程对一个版本的数据做了修改。也会使用redis的SET NX PX TTL,但是曾经出现过TTL设置不合理导致多线程并发执行。看一个大神写的文章提到了redis官方推荐的redisson框架,使用lua脚本来加锁,保证原子性,支持锁的可重入,可以支持redis cluster、master-slave、redis哨兵和redis单机模式。还会有一个watchDog线程去自动延长业务线程还占有锁的TTL,默认TTL是30s。
1)spring方式
1.引入maven依赖和定义配置文件
org.redisson
redisson
3.11.1
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.PropertySources;
@Configuration
//1.引入配置文件
@PropertySources({
//redisson配置
@PropertySource(encoding = "UTF-8", value = "classpath:redisson-${spring.profiles.active:production}.properties")
})
//2.二选一 或者选xml
@ImportResource({
"classpath:spring/application-context-redisson.xml"
})
public class ResourcesConfig {
}
redis.comm.sentinel1=xxx.xxx.xxx.xxx:xxx
redis.comm.sentinel2=xxx.xxx.xxx.xxx:xxx
redis.comm.sentinel3=xxx.xxx.xxx.xxx:xxx
redis.comm.sentinel.mastername=myMaster
redis.comm.password=myPassword
2.定义分布式锁的接口
/**
* @author leijing
* @date 2019/5/8
* @description 分布式锁接口
*/
public interface IDistributedLocker {
boolean lock(String lockKey);
void unlock(String lockKey);
boolean lock(String lockKey, int timeout);
boolean lock(String lockKey, TimeUnit unit ,int timeout);
}
3.定义分布式锁的实现类
import cn.yy.ent.peiwan.config.redisson.RedissonConfig;
import cn.yy.ent.peiwan.lock.IDistributedLocker;
import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SentinelServersConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @author leijing
* @date 2019/5/10
* @description redisson分布式锁实现
*/
@Component
public class RedissonDistributedLocker implements IDistributedLocker {
private static final Logger logger = LoggerFactory.getLogger(RedissonDistributedLocker.class);
private RedissonClient redissonClient;
private volatile boolean isInitted;
public RedissonDistributedLocker(RedissonConfig config){
init(config);
}
public RedissonDistributedLocker(){
}
private void init(RedissonConfig redissonConfig){
if (!isInitted) {
logger.info("init RedissonDistributedLocker,redissonConfig:{}",redissonConfig);
Config config = new Config();
SentinelServersConfig serverConfig = config.useSentinelServers().addSentinelAddress(redissonConfig.getSentinelAddresses())
.setMasterName(redissonConfig.getMasterName())
.setTimeout(redissonConfig.getTimeout())
.setMasterConnectionPoolSize(redissonConfig.getMasterConnectionPoolSize())
.setSlaveConnectionPoolSize(redissonConfig.getSlaveConnectionPoolSize())
.setSlaveConnectionMinimumIdleSize(redissonConfig.getConnectionMinimumIdleSize());
if (StringUtils.isNotBlank(redissonConfig.getPassword())) {
serverConfig.setPassword(redissonConfig.getPassword());
}
redissonClient = Redisson.create(config);
isInitted = true;
}
}
@Override
public boolean lock(String lockKey){
RLock lock = redissonClient.getLock(lockKey);
boolean success = lock.tryLock();
logger.info("lock lockKey:{},success:{}",lockKey,success);
return success;
}
@Override
public void unlock(String lockKey) {
try {
RLock lock = redissonClient.getLock(lockKey);
lock.unlock();
logger.info("unlock lockKey:{}", lockKey);
}catch (Exception e){
logger.error("unlock error,lockKey:{},e:{}",lockKey , e);
}
}
@Override
public boolean lock(String lockKey, int leaseTime){
boolean success = false;
RLock lock = redissonClient.getLock(lockKey);
try {
success = lock.tryLock(leaseTime, TimeUnit.SECONDS);
}catch(InterruptedException e){
logger.error("lock lockKey error,lockKey:{},leaseTime:{},e:{}",lockKey,leaseTime,e);
}
return success;
}
@Override
public boolean lock(String lockKey, TimeUnit unit ,int timeout) {
boolean success = false;
RLock lock = redissonClient.getLock(lockKey);
try{
success = lock.tryLock(timeout, unit);
}catch(InterruptedException e){
logger.error("lock lockKey:{},unit:{},timeout:{} error,",lockKey,unit,timeout,e);
}
return success;
}
public RedissonClient getRedissonClient() {
return redissonClient;
}
public boolean isInitted(){
return isInitted;
}
}
4.定义分布式锁的注解
package cn.yy.ent.peiwan.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* @author leijing
* @date 2019/5/13
* @description 分布式锁注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.METHOD
})
@Documented
public @interface EnableDistributedLocker {
String value();
int timeOut() default -1;
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
5.定义aspect(使用aop注解)
import cn.yy.ent.peiwan.annotation.EnableDistributedLocker;
import cn.yy.ent.peiwan.lock.impl.RedissonDistributedLocker;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
/**
* @author leijing
* @date 2019/5/13
* @description 对所有EnableDistributedLocker注解的方法做切面
*/
@Aspect
@Component
public class DistributedLockerAspect {
private static final Logger logger = LoggerFactory.getLogger(DistributedLockerAspect.class);
@Autowired(required = false)
private RedissonDistributedLocker distributedLocker;
private static final String EXCEPTION_MSG = "请求已经在处理,不要重复提交!";
/**
* 注解的方法
*/
@Pointcut("execution(@cn.yy.ent.peiwan.annotation.EnableDistributedLocker * *.*(..))")
public void methodAnnotatedLocker() {
}
@Around("methodAnnotatedLocker() && @annotation(lockerAnnotation)")
public Object adviseAnnotatedMethods(ProceedingJoinPoint pjp, EnableDistributedLocker lockerAnnotation) throws Throwable {
Object rt = null;
String lockKey = null;
try{
MethodSignature signature = (MethodSignature) pjp.getSignature();
String[] paramNames =signature.getParameterNames();
Object[] args = pjp.getArgs();
String el = lockerAnnotation.value();
//解析el表达式,将#id等替换为参数值
ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression(el);
EvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i
6.定义分布式锁异常类
package cn.yy.ent.peiwan.lock.exception;
/**
* @author: leijing
* @date: 2019/5/13
* @description: 分布式锁异常类
*/
public class DistributedLockException extends Exception{
private String msg ;
public DistributedLockException(String msg){
super(msg);
this.msg = msg;
}
public String getMsg() {
return msg;
}
}
7.定义一个用于测试的服务
/**
* @author leijing
* @date 2019/5/8
* @description测试服务
*/
@Service
public class LightPcActService{
private static final Logger logger = LoggerFactory.getLogger(LightPcActService.class);
@EnableDistributedLocker(value = "'sleep_' + #uid + '_' + #time")
public boolean sleep(long uid , int time){
boolean success = false;
try{
Thread.sleep(time);
success = true;
}catch (Exception e){
success = false;
}
logger.info("sleep success:{}",success);
return success;
}
@EnableDistributedLocker(value = "'testObj_' + #source.uid + '_' + #source.eventType")
public boolean testObj(LightEventSource source){
boolean success = false;
try{
Thread.sleep(5000);
success = true;
}catch (Exception e){
success = false;
}
logger.info("testObj success:{}",success);
return success;
}
}
8.测试类
import cn.yy.ent.peiwan.Main;
import cn.yy.ent.peiwan.light.entity.EventTypeEnum;
import cn.yy.ent.peiwan.light.entity.LightEventSource;
import cn.yy.ent.peiwan.light.entity.common.LightLevelConfig;
import cn.yy.ent.peiwan.lock.IDistributedLocker;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Maps;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.Map;
/**
* @author leijing
* @date 2019/5/9
* @description 分布式锁测试类
*/
@SpringBootTest(classes={Main.class})
@RunWith(SpringJUnit4ClassRunner.class)
public class LightPcActTest {
@Autowired
private LightPcActService lightPcActService;
@Autowired
private IDistributedLocker distributedLocker;
@Test
public void lockTest() {
String key = "test";
distributedLocker.lock(key);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
distributedLocker.unlock(key);
}
@Test
public void testSleep()throws InterruptedException{
try{
boolean success = lightPcActService.sleep(50015831 , 5000);
System.out.println("testSleep success:" + success);
}catch(DistributedLockException e){
System.out.println("testSleep error:" +e.getMsg());
}
}
@Test
public void testObj()throws InterruptedException{
LightEventSource source = new LightEventSource(50015831 , "4.0.9",1 , EventTypeEnum.USER_LOGIN.getCode());
try{
boolean success = lightPcActService.testObj(source);
System.out.println("testObj success:" + success);
}catch(DistributedLockException e){
System.out.println("testObj error:" +e.getMsg());
}
}
}
2)SpringBoot方式:
1.引入maven依赖
org.redisson
redisson
3.11.1
2.定义配置类
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
/**
* @author leijing
* @date 2019/5/10
* @description redisson属性装配类
*/
@ConfigurationProperties(prefix = "redisson")
public class RedissonProperties {
private int timeout = 3000;
private String address;
private String password;
private int connectionPoolSize = 64;
private int connectionMinimumIdleSize=10;
private int slaveConnectionPoolSize = 250;
private int masterConnectionPoolSize = 250;
private String[] sentinelAddresses;
private String masterName;
public RedissonProperties(){
System.out.println("RedissonProperties init.......");
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public int getSlaveConnectionPoolSize() {
return slaveConnectionPoolSize;
}
public void setSlaveConnectionPoolSize(int slaveConnectionPoolSize) {
this.slaveConnectionPoolSize = slaveConnectionPoolSize;
}
public int getMasterConnectionPoolSize() {
return masterConnectionPoolSize;
}
public void setMasterConnectionPoolSize(int masterConnectionPoolSize) {
this.masterConnectionPoolSize = masterConnectionPoolSize;
}
public String[] getSentinelAddresses() {
return sentinelAddresses;
}
public void setSentinelAddresses(String sentinelAddresses) {
this.sentinelAddresses = sentinelAddresses.split(",");
}
public String getMasterName() {
return masterName;
}
public void setMasterName(String masterName) {
this.masterName = masterName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public int getConnectionPoolSize() {
return connectionPoolSize;
}
public void setConnectionPoolSize(int connectionPoolSize) {
this.connectionPoolSize = connectionPoolSize;
}
public int getConnectionMinimumIdleSize() {
return connectionMinimumIdleSize;
}
public void setConnectionMinimumIdleSize(int connectionMinimumIdleSize) {
this.connectionMinimumIdleSize = connectionMinimumIdleSize;
}
}
3、根据配置注入RedissonClient
import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SentinelServersConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author leijing
* @date 2019/5/10
* @description Redisson配置类
*/
@Configuration
@EnableConfigurationProperties(cn.yy.ent.peiwan.config.redisson.RedissonProperties.class)
public class RedissonConfig {
@Autowired
private RedissonProperties redssionProperties;
/**
* 哨兵模式自动装配
* @return
*/
@Bean
RedissonClient redissonSentinel() {
Config config = new Config();
SentinelServersConfig serverConfig = config.useSentinelServers().addSentinelAddress(redssionProperties.getSentinelAddresses())
.setMasterName(redssionProperties.getMasterName())
.setTimeout(redssionProperties.getTimeout())
.setMasterConnectionPoolSize(redssionProperties.getMasterConnectionPoolSize())
.setSlaveConnectionPoolSize(redssionProperties.getSlaveConnectionPoolSize())
.setSlaveConnectionMinimumIdleSize(redssionProperties.getConnectionMinimumIdleSize());
if (StringUtils.isNotBlank(redssionProperties.getPassword())) {
serverConfig.setPassword(redssionProperties.getPassword());
}
return Redisson.create(config);
}
}