多级缓存分析篇(二) 常用分布式锁分析

上篇主要讲了日常经常使用的哪些redis包及其配置的差异,这篇对基于redis的分布式锁,主要分享下自定义锁和redisson锁的使用和分析。

1.(自定义)Redis分布式锁

对于redis来说,非常适合做分布式锁来控制各个服务并发请求下造成的变量不一致、会串的问题。zookeeper锁其实也可以但不是很推荐,毕竟zk客户端是有某节点宕机自愈后、数据分发不同步的问题。

Redis锁的原理其实就是SETNX的原子性控制,建议一定要 设定超时时间,防止资源竞争过于激烈导致线程长时间获取不到锁、发生timeout最终导致死锁。而像Mysql数据库采用的是InnoDB模式,默认参数innodb_lock_wait_timeout设置锁等待的时间是50s,一旦数据库锁超过这个时间就会报错。

SETNX的完整语法:SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]

必选参数说明:

  • SET:命令
  • key:待设置的key
  • value:设置的key的value,最好为随机字符串

可选参数说明:

  • NX:表示key不存在时才设置,如果存在则返回 null

  • XX:表示key存在时才设置,如果不存在则返回NULL

  • PX millseconds:设置过期时间,过期时间精确为毫秒

  • EX seconds:设置过期时间,过期时间精确为秒

这里提供一个自定义的Redis锁给大家参考使用:

package com.test.common.utils;


import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisConnectionUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.UUID;


@Component
@Slf4j
public class RedisLockUtil {

    @Autowired
    private RedisTemplate redisTemplate;

	/**
	 * 默认锁超时时间(毫秒)
	 */
	private static final int DEFAULT_EXPIRE = 5*1000;


	/**
	 * 锁名称前缀
	 */
	private static final String LOCK_PREFIX = "Coupon:";

	private RedisLock() {
	}

	/**
	 * 获取锁
	 * @param lockName	锁名称
	 * @param acquireTimeout	等待获取锁的超时时间(毫秒)
	 * @return	加锁成功后返回锁的唯一标识,未获取成功则返回null
	 */
	public String lock(String lockName, long acquireTimeout) {
		return lockWithTimeout(lockName, acquireTimeout, DEFAULT_EXPIRE);
	}



	/**
	 * 获取锁
	 * @param lockName	锁名称
	 * @param acquireTimeout	等待获取锁的超时时间(毫秒)
	 * @param timeout	锁超时时间,上锁后超过此时间则自动释放锁
	 * @return	加锁成功后返回锁的唯一标识,未获取成功则返回null
	 */
	public String lockWithTimeout(String lockName, long acquireTimeout, long timeout){

		RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
		RedisConnection redisConnection = connectionFactory.getConnection();

		/** 随机生成一个value */
		String identifier = UUID.randomUUID().toString();
		String lockKey = LOCK_PREFIX + lockName;
		int lockExpire = (int)(timeout / 1000);

		long end = System.currentTimeMillis() + acquireTimeout;	/** 获取锁的超时时间,超过这个时间则放弃获取锁 */
		while (System.currentTimeMillis() < end) {
			if (redisConnection.setNX(lockKey.getBytes(), identifier.getBytes())) {
				redisConnection.expire(lockKey.getBytes(), lockExpire);
				/** 获取锁成功,返回标识锁的value值,用于释放锁确认 */
				RedisConnectionUtils.releaseConnection(redisConnection, connectionFactory);
				return identifier;
			}
			/** 返回-1代表key没有设置超时时间,为key设置一个超时时间 */
			if (redisConnection.ttl(lockKey.getBytes()) == -1) {
				redisConnection.expire(lockKey.getBytes(), lockExpire);
			}
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				log.warn("获取分布式锁:线程中断!");
				Thread.currentThread().interrupt();
			}
		}
		RedisConnectionUtils.releaseConnection(redisConnection, connectionFactory);
		return null;
	}

	/**
	 * 释放锁
	 * @param lockName 锁名称
	 * @param identifier 锁标识
	 * @return
	 */
	public boolean releaseLock(String lockName, String identifier) {

		if (StringUtils.isEmpty(identifier)) return false;

		RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
		RedisConnection redisConnection = connectionFactory.getConnection();
		String lockKey = LOCK_PREFIX + lockName;
		boolean releaseFlag = false;
		while (true) {
			try{
				/** 监视lock,准备开始事务 */
				//redisConnection.watch(lockKey.getBytes());
				byte[] valueBytes = redisConnection.get(lockKey.getBytes());

				/**  value为空表示锁不存在或已经被释放*/
				if(valueBytes == null){
					//redisConnection.unwatch();
					releaseFlag = false;
					break;
				}

				/** 通过前面返回的value值判断是不是该锁,若是该锁,则删除,释放锁 */
				String identifierValue = new String(valueBytes);
				if (identifier.equals(identifierValue)) {
					//redisConnection.multi();
					redisConnection.del(lockKey.getBytes());
//					List<Object> results = redisConnection.exec();
//					if (results == null) {
//						continue;
//					}
					releaseFlag = true;
				}
//				redisConnection.unwatch();
				break;
			}
			catch(Exception e){
				log.warn("释放锁异常", e);
			}
		}
		RedisConnectionUtils.releaseConnection(redisConnection, connectionFactory);
		return releaseFlag;
	}
}

使用Redis分布式锁用于领券场景的单元测试类:

package com.test.marketing.svc.client;

import com.test.marketing.base.client.RedisLock;
import com.test.marketing.base.entity.CouponSummary;
import com.test.marketing.svc.init.MarketingServiceApplication;
import org.apache.commons.lang3.StringUtils;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;
import java.util.stream.IntStream;

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MarketingServiceApplication.class)
public class RedisLockTest {

    @Resource
    private RedisLock redisLock;


    /**
     *  测试并发领券/抢券场景下,使用redis分布锁累计领券总数
     */
    @Test
    public void testCountReceive() {


        CouponSummary couponSummary = new CouponSummary();
        couponSummary.setReceivedQuantity(0l);

        IntStream.range(0,50).parallel()
                .forEach( o->countReceiveQuantity(couponSummary) );
        log.info("Count Received quantities :" + couponSummary.getReceivedQuantity());
        Assert.assertNotEquals(50l, couponSummary.getReceivedQuantity().longValue());

        couponSummary.setReceivedQuantity(0l);
        IntStream.range(0,50).parallel()
                .forEach( o->countReceiveQuantityWithLock("test",1000L, couponSummary) );
        log.info("Count Received quantities with lock :" + couponSummary.getReceivedQuantity());
        Assert.assertEquals(50l, couponSummary.getReceivedQuantity().longValue());
    }

    private void countReceiveQuantityWithLock(String lockName, Long acquireTimeout, CouponSummary couponSummary)  {

        String lockIdentify = redisLock.lock(lockName,acquireTimeout);
        if (StringUtils.isNotEmpty(lockIdentify)){
            countReceiveQuantity(couponSummary);
            redisLock.releaseLock(lockName, lockIdentify);
        }
        else{
            log.error("get lock failed.");
        }

    }

    private void countReceiveQuantity(CouponSummary couponSummary){

        couponSummary.setReceivedQuantity(couponSummary.getReceivedQuantity()+1);
        try {
            Thread.sleep(10l);
        } catch (InterruptedException e) {
            log.error("count thread was interrupted");
        }

    }

}

2.Redisson分布式锁

Redisson是比较不错的开源框架,一般用于分布式锁或注册中心控制。Redisson实现的是可重入锁,接口RLock通过RedissonLock类来实现主要逻辑的,

2.1 使用案例

Redisson的使用方式其实很简单:

    @Resource
    private RedissonClient redissonClient;

    
    //如下为代码片段:

        //若不加锁则会产生超卖
        RLock lock = redissonClient.getLock("stock:" + productId);

        try {
            lock.lock(10, TimeUnit.SECONDS);

            int stockAmount = stockService.get(productId).getStockAmount();
            log.info("剩余库存量为:{}", stockAmount);

            if (stockAmount <= 0) {
                return false;
            }

        String orderNo = UUID.randomUUID().toString().replace(".", "").toUpperCase();

        /*
            原子性操作(例如减库存操作等)...

        */

        } catch (Exception ex) {
            log.error("下单失败", ex);
        } finally {
            lock.unlock();
        }

备注:这里的getLock()默认是指的非公平锁,redisson也提过了公平锁getFairLock和多锁getMultiLock():RedissonMultiLock,也支持异步方式lock.tryLockAsync(100,10,TimeUnit.SECONDS):Future<T>,很方便使用。

其它细节更多可参考:Redisson基本用法 - 废物大师兄 - 博客园

2.2 配置Redisson

Redisson推荐redis集群节点至少为3个(奇数个),redis cluster集群版本个人推荐在5.0.3以上。

YAML配置方式(redisson.yml):

clusterServersConfig:
  idleConnectionTimeout: 10000
  pingTimeout: 1000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  reconnectionTimeout: 3000
  failedAttempts: 3
  password: null
  subscriptionsPerConnection: 5
  clientName: null
  loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
  subscriptionConnectionMinimumIdleSize: 1
  subscriptionConnectionPoolSize: 50
  slaveConnectionMinimumIdleSize: 5
  slaveConnectionPoolSize: 128
  masterConnectionMinimumIdleSize: 5
  masterConnectionPoolSize: 128
  readMode: "SLAVE"
  subscriptionMode: "SLAVE"
  nodeAddresses:
    - "redis://172.17.17.5:30004"
    - "redis://172.17.17.5:30005"
    - "redis://172.17.17.5:30006"
  scanInterval: 1000
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.SnappyCodec> {}

配置类方式:

/**
 * redisson配置类
 */
@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
       Config config = new Config();
       config.useClusterServers()
               .setScanInterval(2000)
               .addNodeAddress("redis://10.102.5.11:6379","redis://10.102.5.11:6380")
               .addNodeAddress("redis://redis://10.102.5.11:6381");
       RedissonClient redisson = Redisson.create(config);
       return redisson;
    }

}

好了,基于redis的分布式锁就讨论到这里,下一讲还是回归大本营,谈谈Spring官方对Cache的顶层设计及其Redis的桥接是如何实现的 多级缓存分析篇(三) Spring本地缓存源码分析,欢迎来交流留言哦~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值