java分布式锁的实现及在springboot中的应用
redis
分布式实现原理
SET resource_name my_random_value NX PX 30000
NX
:表示只有key
不存在的时候才会设置成功。(如果此时 redis 中存在这个 key,那么设置失败,返回nil
)PX 30000
:意思是 30s 后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。random_value
随机值,判断 value 一样才删除
jedis实现
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.5.0</version>
</dependency>
public class JedisUtil {
private static JedisPool jedisPool;
static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(10000);
jedisPoolConfig.setMaxWaitMillis(10000);
jedisPoolConfig.setMaxIdle(1000);
jedisPool = new JedisPool(jedisPoolConfig,"127.0.0.1",6379,100000);
}
public static Jedis getRerouse(){
return jedisPool.getResource();
}
}
public class JedisDistributeLock {
private static final Logger logger = LoggerFactory.getLogger(JedisDistributeLock.class);
private static final String SUCCESS ="OK";
private static final String SET_IF_NOT_EXIST ="NX";
private static final String SET_WITH_EXPIRE_TIME ="EX";
//释放锁成功标示
private static final Long RELEASE_SUCCESS = 1L;
//获取锁时的睡眠等待时间片,单位毫秒
private static final long SLEEP_PER = 5;
//默认过期时间
public static final int DEFAULT_EXPIRE_1000_Milliseconds = 1000;
private ThreadLocal<String> threadLocal = new ThreadLocal<>();
private void setThreadLocal(String uuid){
threadLocal.set(uuid);
}
private String getThreadLocal(){
return threadLocal.get();
}
public void acquire(String key){
String value = UUID.randomUUID().toString();
lock(key,value,5000);
setThreadLocal(value);
}
public void release(String key){
String value = getThreadLocal();
unlock(key,value);
}
private Boolean tryGetLock(Jedis jedis,String key,String requestId,int expireTime){
String result = jedis.set(key,requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,expireTime);
if(SUCCESS.equals(result)){
return true;
}
return false;
}
private Boolean releaseLock(Jedis jedis,String key,String requestId){
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(requestId));
if(RELEASE_SUCCESS.equals(result)){
return true;
}
return false;
}
private void lock(String key,String value,int expireTime){
try (Jedis jedis = JedisUtil.getRerouse()){
while(!tryLock(key,value,expireTime)){
//缺点,睡眠期间其它线程释放锁不能及时收到
try {
Thread.sleep(SLEEP_PER);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private boolean tryLock(String key,String value,int expireTime){
try (Jedis jedis = JedisUtil.getRerouse()){
return tryGetLock(jedis,key,value,expireTime);
}
}
private void unlock(String key,String value){
try (Jedis jedis = JedisUtil.getRerouse()){
releaseLock(jedis,key,value);
}
}
public static void main(String[] args) {
JedisDistributeLock lock = new JedisDistributeLock();
try {
lock.acquire("order");
logger.info("01获取锁");
Thread thread = new Thread(new Runnable() {
public void run() {
try {
lock.acquire("order");
logger.info("02获取锁");
lock.release("order");
logger.info("02释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread.start();
Thread.sleep(2000);
lock.release("order");
logger.info("01释放锁");
}catch (Exception e){
e.printStackTrace();
}
}
}
lettuce结合springboot实现
springboot2.x后redis客户端默认使用lettuce
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
//设置序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer); // key序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // value序列化
redisTemplate.setHashKeySerializer(stringSerializer); // Hash key序列化
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); // Hash value序列化
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
@Component
public class SpringRedisLock {
private static final Logger logger = LoggerFactory.getLogger(SpringRedisLock.class);
private static final RedisScript<Long> lua_lock = new DefaultRedisScript<>("if redis.call(\"setnx\", KEYS[1], KEYS[2]) == 1 then return redis.call(\"pexpire\", KEYS[1], KEYS[3]) else return 0 end", Long.class);
private static final RedisScript<Long> lua_unlock = new DefaultRedisScript<>("if redis.call(\"get\",KEYS[1]) == KEYS[2] then return redis.call(\"del\",KEYS[1]) else return -1 end", Long.class);
@Autowired
private RedisTemplate redisTemplate;
Integer timeOut =1;
ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 多台机器的情况下,会出现大量的等待,加重redis的压力。 在lock方法上,加入同步关键字。单机同步,多机用redis
Lock lock = new ReentrantLock();
public void lock(String key ) {
try {
lock.lock();
while(!tryLock(key)){
redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
CountDownLatch waiter = new CountDownLatch(1);
// 等待通知结果,使用jedis在此处会阻塞
connection.subscribe((message, pattern) -> {
// 收到通知,不管结果,立刻再次抢锁
waiter.countDown();
}, (key + "_unlock_channel").getBytes());
try {
// 等待一段时间,超过这个时间都没收到消息,肯定有问题
waiter.await(timeOut, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return true; //随便返回一个值都没问题
}
});
logger.info("继续下一次循环");
}
} finally {
lock.unlock();
}
}
public boolean tryLock(String key ) {
String value = UUID.randomUUID().toString();
List<String> keys = Arrays.asList(key, value, String.valueOf(1000));
Long result = (Long) redisTemplate.execute(lua_lock,keys);
if(result==1){
logger.info(Thread.currentThread().getName() + "获取到锁");
threadLocal.set(value);
return true;
}else{
return false;
}
}
public void unlock(String key) {
//1、 要比对内部的值,同一个线程,才能够去释放锁。 2、 同时发出通知
String value = threadLocal.get();
try {
List<String> keys = Arrays.asList(key, value);
Long result = (Long) redisTemplate.execute(lua_unlock,keys);
if(result !=-1){
logger.info(Thread.currentThread().getName() + "释放锁");
redisTemplate.execute(new RedisCallback() {
@Override
public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
redisConnection.publish((key + "_unlock_channel").getBytes(),"".getBytes());
return null;
}
});
}
}finally {
threadLocal.remove();
}
}
}
测试类
@Service
public class TicketService {
@Autowired
private SpringRedisLock springRedisLock;
private Long tickets = 0l;
public void buyTicket(String userId) {
try {
springRedisLock.lock("ticket");
System.out.println(userId + "正在买第 " + ++tickets + " 张票");
} catch (Exception e) {
e.printStackTrace();
} finally {
springRedisLock.unlock("ticket");
}
}
}
@Test
public void testRe(){
int currency = 200;
CyclicBarrier cyclicBarrier = new CyclicBarrier(currency);
for (int i = 0; i <currency ; i++) {
String userId = "Tony" +i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"线程准备好");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
ticketService.buyTicket(userId);
}).start();
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
redission实现
实现原理如下
代码
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.9.1</version>
</dependency>
public class RedissionLock {
private static final Logger logger = LoggerFactory.getLogger(RedissionLock.class);
private RLock rLock;
private RedissionLock(String key) {
rLock = getRlock(key);
}
private RLock getRlock(String key) {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient client = Redisson.create(config);
RLock rLock = client.getLock("order");
return rLock;
}
public void acquire() {
rLock.lock();
}
public void release() {
rLock.unlock();
}
public static void main(String[] args) {
RedissionLock lock = new RedissionLock("order");
try {
lock.acquire();
logger.info("01获取锁");
Thread thread = new Thread(new Runnable() {
public void run() {
try {
lock.acquire();
logger.info("02获取锁");
lock.release();
logger.info("02释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread.start();
Thread.sleep(2000);
lock.release();
logger.info("01释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
}
redlock
分布式架构中的CAP理论,分布式系统只能同时满足两个: 一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。
上述redis分布式锁是AP模式,当锁存在的redis节点宕机,可能会被误判为锁失效,或者没有加锁。具体如下:
- A客户端请求主节点获取到了锁
- 主节点挂掉了,但是还没把锁的信息同步给其他从节点
- 由于主节点挂了,这时候开始主从切换,从节点成为主节点继续工作,但是新的主节点上,没有A客户端的加锁信息
- 这时候B客户端来加锁,因为目前是一个新的主节点,上面没有其他客户端加锁信息,所以B客户端获取锁成功
- 这时候就存在问题了,A和B两个客户端同时都持有锁,同时在执行代码,那么这时候分布式锁就失效了。
在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。现在我们假设有5个Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。
RedLock算法思想,意思是不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,n / 2 + 1,必须在大多数redis节点上都成功创建锁,才能算这个整体的RedLock加锁成功,避免说仅仅在一个redis实例上加锁而带来的问题。
redisson已经有对redlock算法封装。具体使用如下:
Config config = new Config();
config.useSentinelServers().addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389")
.setMasterName("masterName")
.setPassword("password").setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
RLock redLock = redissonClient.getLock("REDLOCK_KEY");
boolean isLock;
try {
isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
if (isLock) {
//TODO if get lock success, do something;
}
} catch (Exception e) {
} finally {
redLock.unlock();
}
redlock可能失效的场景
- 某个节点没有持久化导致锁失效,A、B、C、D、E,user01 锁住A、B、C,而后C宕机重启,user02锁住C、D、E,可以使用延时重启(延时时间要结合业务锁失效时间)
- 某个节点发生时钟跳跃导致锁失效
- 在锁释放前已经过了失效时间(单机也有这种问题),比如GC、操作系统cpu上下文切换、网络延时
zookeeper
临时节点
多个客户端去创建相同的临时节点,创建成功则表示拿到了锁
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
public class ZkLock {
private static final Logger logger = LoggerFactory.getLogger(ZkLock.class);
private ZkClient zkClient;
private String lockPath;
private final static String ROOT_PATH = "/lock/";
private ZkLock(String lockPath){
zkClient = new ZkClient("127.0.0.1:2181");
zkClient.setZkSerializer(new MyZkSerializer());
this.lockPath = ROOT_PATH + lockPath;
}
public void lock() {
if(!tryLock()){
waitForLock();
lock();
}
}
public void unlock() {
zkClient.delete(lockPath);
}
public boolean tryLock() {
try {
zkClient.createEphemeral(lockPath);
} catch (ZkNodeExistsException e) {
return false;
}
return true;
}
private void waitForLock(){
CountDownLatch countDownLatch = new CountDownLatch(1);
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
logger.info(s+ "节点被删除了");
countDownLatch.countDown();
}
};
zkClient.subscribeDataChanges(lockPath,listener);
if(zkClient.exists(lockPath)){
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
zkClient.unsubscribeDataChanges(lockPath,listener);
}
public static void main(String[] args) {
ZkLock zkLock01 = new ZkLock("order");
ZkLock zkLock02 = new ZkLock("order");
try {
zkLock01.lock();
logger.info("zkLock01获取锁");
Thread thread = new Thread(new Runnable() {
public void run() {
try {
zkLock02.lock();
logger.info("zkLock02第一次获取锁");
zkLock02.unlock();
logger.info("zkLock02第一次释放锁");
zkLock02.lock();
logger.info("zkLock02第二次获取锁");
zkLock02.unlock();
logger.info("zkLock02第二次释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread.start();
Thread.sleep(2000);
zkLock01.unlock();
logger.info("zkLock01释放锁");
}catch (Exception e){
e.printStackTrace();
}
}
}
临时顺序节点
多个客户端在一个节点下创建临时顺序节点,按照节点顺序依次获取锁,避免了惊群效应,提高了性能
public class ZkLock02 {
private static final Logger logger = LoggerFactory.getLogger(ZkLock02.class);
private String lockPath;
private ZkClient zkClient;
private String beforePath;
private String currentPath;
private final static String ROOT_PATH = "/lock/";
public ZkLock02(String path){
this.lockPath = ROOT_PATH +path;
zkClient = new ZkClient("127.0.0.1:2181");
zkClient.setZkSerializer(new MyZkSerializer());
if(!zkClient.exists(lockPath)){
try {
zkClient.createPersistent(lockPath);
} catch (RuntimeException e) {
e.printStackTrace();
}
}
}
public void unlock() {
zkClient.delete(currentPath);
}
public boolean tryLock() {
if(currentPath==null){
currentPath = zkClient.createEphemeralSequential(lockPath+"/","asd");
}
List<String> childs = zkClient.getChildren(lockPath).stream().sorted((a, b)->a.compareTo(b)).collect(Collectors.toList());
if(currentPath.equals(lockPath +"/" + childs.get(0))){
return true;
}else{
int curIndex = childs.indexOf(currentPath.substring(lockPath.length()+1));
beforePath = lockPath +"/" + childs.get(curIndex-1);
}
return false;
}
public void lock() {
if(!tryLock()){
waitForLock();
lock();
}
}
private void waitForLock(){
CountDownLatch countDownLatch = new CountDownLatch(1);
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
logger.info(s+ "节点被删除了");
countDownLatch.countDown();
}
};
zkClient.subscribeDataChanges(beforePath,listener);
if(zkClient.exists(beforePath)){
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
zkClient.unsubscribeDataChanges(beforePath,listener);
}
public static void main(String[] args) {
ZkLock02 zkLock01 = new ZkLock02("order");
ZkLock02 zkLock02 = new ZkLock02("order");
ZkLock02 zkLock03 = new ZkLock02("order");
try {
zkLock01.lock();
logger.info("zkLock01获取锁");
Thread thread = new Thread(new Runnable() {
public void run() {
try {
zkLock02.lock();
logger.info("zkLock02第一次获取锁");
zkLock02.unlock();
logger.info("zkLock02第一次释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread.start();
Thread thread02 = new Thread(new Runnable() {
public void run() {
try {
zkLock03.lock();
logger.info("zkLock03第一次获取锁");
zkLock03.unlock();
logger.info("zkLock03第一次释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread02.start();
Thread.sleep(2000);
zkLock01.unlock();
logger.info("zkLock01释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
public void close(){
zkClient.close();
}
}
curator实现及与springboot集成
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.8.0</version>
</dependency>
@Configuration
public class CuratorConfiguration {
@Value("${curator.retryCount}")
private int retryCount;
@Value("${curator.elapsedTimeMs}")
private int elapsedTimeMs;
@Value("${curator.connectString}")
private String connectString;
@Value("${curator.sessionTimeoutMs}")
private int sessionTimeoutMs;
@Value("${curator.connectionTimeoutMs}")
private int connectionTimeoutMs;
@Bean(initMethod = "start")
public CuratorFramework curatorFramework() {
return CuratorFrameworkFactory.newClient(
connectString,
sessionTimeoutMs,
connectionTimeoutMs,
new RetryNTimes(retryCount, elapsedTimeMs));
}
}
@Service
public class DistributedLockUtil {
private static final Logger logger = LoggerFactory.getLogger(DistributedLockUtil.class);
private final static String ROOT_PATH_LOCK = "/lock/";
@Autowired
private CuratorFramework curatorFramework;
/**
* InterProcessMutex:分布式可重入排它锁
* InterProcessSemaphoreMutex:分布式排它锁
* InterProcessReadWriteLock:分布式读写锁
* InterProcessMultiLock:将多个锁作为单个实体管理的容器
*/
public InterProcessMutex getLock(String path){
return new InterProcessMutex(curatorFramework, ROOT_PATH_LOCK + path);
}
}
测试代码
@Service
public class OrderNumberService {
private int number = 0;
@Autowired
DistributedLockUtil distributedLockUtil;
public String generateNo(){
String orderNumber ="";
try {
Thread.sleep(new Random().nextInt(500));
} catch (InterruptedException e) {
e.printStackTrace();
}
orderNumber = "订单号:"+ (++number);
// InterProcessMutex lock = distributedLockUtil.getLock("order");
// try{
// lock.acquire();
// orderNumber = "订单号:"+ (++number);
// }catch (Exception e){
// e.printStackTrace();
// }
// finally {
// try {
// lock.release();
// } catch (Exception exception) {
// exception.printStackTrace();
// }
// }
//
return orderNumber;
}
}
@Test
public void testZk(){
int currency = 200;
CyclicBarrier cyclicBarrier = new CyclicBarrier(currency);
for (int i = 0; i <currency ; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"线程准备好");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
String s = orderNumberService.generateNo();
System.out.println(s);
}).start();
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
比较
-
redis 分布式锁,每次锁释放后,每个client端都会去尝试获取锁。zookeeper可以使用顺序节点解决这个问题。
-
redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。
-
redis本身的读写性能很高,因此基于redis的分布式锁效率比较高
-
redis中为了解决单点故障引发的脏数据问题,需要引入redlock,而redlock需要多个独立的master节点;需要同时对多个节点申请锁,降低了一些效率 ,同时加大了资源的消耗。
-
个人觉得单从性能考虑或者公司技术栈考虑(毕竟引入redis的概率比zk高很多),可以先择使用redis,其它情况应该选择更靠高的zookeeper,性能要求不高的话可以考虑基于数据库实现分布式锁。