什么是分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。
redis分布式锁具有什么特点
- redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。
- 高可用、高性能的加锁和解锁。
- SETNX指令和lua脚本,都是原子性操作。
- 具备锁失效机制,通过失效时间,可以有效的避免异常时的死锁问题。
- 具有重入性,如何操作可以通过lua脚本对KEY值进行判断,实现重入和锁有效时间的更新,具体可以看下Redisson中的这段代码。
<T> Future<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId,
RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
//使用 EVAL 命令执行 Lua 脚本获取锁
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime,
getLockName(threadId));
}
使用时注意的问题
1、一些例子分开多步进行key值和有效期的操作,是错误的写法,不能保证原子性 。
2、value值是判断锁使用者身份的证明,最好使用唯一值(如:uuid)的数据,提高识别度,避免出现问题。
redis锁在单机环境多线程下的模拟运用
概念的东西不多做介绍,有许多文章的概念会更加标准和官方,今天的文章以DEMO为主,有的小伙伴会说DEMO为单机环境演示,比较不具备参考性,后续有时间我会补一个实际分布式DEMO更加直观有参考性和学习性。本DEMO为JDK1.8的springboot工程,不会创建springboot和本地没有启动redis的小伙伴我回头弄个简单的教程,先自己查找下教程。
DEMO目录结构图
实例结构
代码部分
pom.xml文件引入依赖如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
配置项目redis环境,redis.properties内容
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=10000
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=200
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=10000
#redis配置结束
spring.redis.block-when-exhausted=true
RedisConfig.java
package com.example.redislock.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
@Configuration
@PropertySource("classpath:config/redis.properties")
@Slf4j
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.jedis.pool.max-wait}")
private long maxWaitMillis;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.block-when-exhausted}")
private boolean blockWhenExhausted;
@Value("${spring.redis.jedis.pool.max-active}")
private int maxActive;
@Value("${spring.redis.jedis.pool.min-idle}")
private int minIdle;
@Bean
public JedisPool redisPoolFactory() throws Exception{
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
jedisPoolConfig.setMaxTotal(maxActive);
jedisPoolConfig.setMinIdle(minIdle);
JedisPool jedisPool = new JedisPool(jedisPoolConfig,host,port,timeout,null);
log.info("JedisPool注入成功!");
log.info("redis地址:" + host + ":" + port);
return jedisPool;
}
}
RedisService.java
package com.example.redislock.service;
public interface RedisService {
boolean lock(String key, String uuid);
boolean unlock(String key, String uuid);
}
RedisServiceImpl.java
package com.example.redislock.service.impl;
import com.example.redislock.service.RedisService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.Collections;
@Service("redisService")
public class RedisServiceImpl implements RedisService {
private static final Logger log = LoggerFactory.getLogger(RedisServiceImpl.class);
@Autowired
private JedisPool jedisPool;
private static final String LOCK_SUCCESS = "OK";
private static final Long RELEASE_SUCCESS = 1L;
//锁过期时间
private static final long expireTime = 30000;
//获取锁的超时时间
private long timeout = 900000;
@Override
public boolean lock(String key, String uuid) {
Jedis jedis = null;
long start = System.currentTimeMillis();
try {
jedis = jedisPool.getResource();
while (true) {
//使用setnx是为了保持原子性
String result = jedis.set(key, uuid, "NX", "PX", expireTime);
//OK标示获得锁,null标示其他任务已经持有锁
if (LOCK_SUCCESS.equals(result)) {
return true;
}
//在timeout时间内仍未获取到锁,则获取失败
long time = System.currentTimeMillis() - start;
if (time >= timeout) {
return false;
}
//增加睡眠时间可能导致结果分散不均匀,测试时可以不用睡眠
// Thread.sleep(1);
}
// }catch (InterruptedException e1) {
// log.error("redis竞争锁,线程sleep异常");
// return false;
} catch (Exception e) {
log.error("redis竞争锁失败");
throw e;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
@Override
public boolean unlock(String key, String uuid) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//lua脚本,使用lua脚本是为了保持原子性
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(key), Collections.singletonList(uuid));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
} catch (Exception e) {
log.error("redis解锁失败");
throw e;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
RedisLockController.java
package com.example.redislock.controller;
import com.example.redislock.service.RedisService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.UUID;
@RestController
public class RedisLockController {
@Resource(name = "redisService")
private RedisService redisService;
//没有连接db讲车票设置为类变量作为共享资源,便于任务竞争进行DEMO展示
private int trainNum = 500;
/**
* 无db,模拟补充车票
* @param num
*/
@RequestMapping(value = "/addTrainNum")
public String addTrainNum(@RequestParam(defaultValue = "500") int num) {
trainNum = trainNum + num;
return "系统已补票,目前车票库存:" + trainNum;
}
/**
* 无锁买票
*/
@RequestMapping(value = "/noLock")
public void noLock() {
while (true) {
if(trainNum > 0) {
try {
Thread.sleep(20);
System.out.println(Thread.currentThread().getName() + "购买了车票,车票号:" + trainNum-- );
} catch (Exception e) {
e.printStackTrace();
} finally {
}
} else {
System.out.println("车票售完");
break;
}
}
}
/**
* redis锁买票
*/
@RequestMapping(value = "/redisLock")
public void redisLock() {
String uuid = UUID.randomUUID().toString();
while (true) {
redisService.lock("train_test", uuid);
if(trainNum > 0) {
try {
Thread.sleep(20);
System.out.println(Thread.currentThread().getName() + "购买了车票,车票号:" + trainNum-- + ",uuid:" + uuid);
} catch (Exception e) {
e.printStackTrace();
} finally {
redisService.unlock("train_test", uuid);
}
} else {
System.out.println("车票售完");
redisService.unlock("train_test", uuid);
break;
}
}
}
/**
* 本地多线程模拟
*/
@RequestMapping(value = "/redisLockTest")
public void redisLockTest() {
Train train = new Train(40);
Thread thread1 = new Thread(train, "小明");
Thread thread2 = new Thread(train, "小王");
Thread thread3 = new Thread(train, "小李");
thread1.start();
thread2.start();
thread3.start();
}
class Train implements Runnable {
private int num;
private ThreadLocal<String> localUUID = new ThreadLocal<>();
public Train(int num) {
this.num = num;
}
@Override
public void run() {
localUUID.set(UUID.randomUUID().toString());
while (true) {
redisService.lock("train_test", localUUID.get());
if(num > 0) {
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "购买了车票,车票号:" + num-- + ",uuid:" + localUUID.get());
} catch (Exception e) {
e.printStackTrace();
} finally {
redisService.unlock("train_test", localUUID.get());
}
} else {
redisService.unlock("train_test", localUUID.get());
System.out.println("车票售完");
break;
}
}
}
}
}
实验步骤
1、启动RedislockApplication
2、网页单次执行 http://localhost:8080/noLock ,查看控制台信息,如下图,购票信息正常。
3、另开一个页签执行 http://localhost:8080/addTrainNum?num=500 补充车票,可以根据自己需要调整。
4、网页快速多次执行(F5刷新) http://localhost:8080/noLock ,查看控制台信息,如下图,购票信息异常。异常原因:类变量在堆中属于共享变量,多线程任务情况下,操作同一资源造成的线程不安全现象。
5、再次打开页签执行 http://localhost:8080/addTrainNum?num=500 补充车票。
6、网页快速多次执行(F5刷新) http://localhost:8080/redisLock 查看控制台信息,如下图,购票信息正常,redis锁生效。
本次DEMO演示到此结束,如果有问题欢迎给我留言,我会及时改正,后面我会补一更全的DEMO