spring 业务锁_基于redis实现分布式锁

今天聊聊redis分布式锁(redis单机版本),锁需要满足一下几点:

a 互斥行:同一时刻只能有一个线程获取锁,只有当该线程执行完业务逻辑释放锁以后,其他线程才能尝试获取锁。

b 保证锁的释放,当A服务器加锁成功后宕机,不能影响其他服务器获取锁,这个可以通过过期时间来设置

c A线程加锁,这个锁只能由A线程去解锁,其他线程不能解锁A线程加的锁,否则就乱套了(如果被其他线程解锁,那么其他线程可能成功获取锁,这样就不是串行执行了)。

下面给出实现代码:

1 引人依赖:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.29</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

2 application.yml配置文件:

server: port: 80spring: datasource: username: rootpassword: 123456url: jdbc:mysql://192.168.137.129:3306/my_dbtest?useUnicode=true&characterEncoding=utf-8driver-class-name: com.mysql.jdbc.Driverredis: database: 0host: 127.0.0.1port: 6379password: jedis: pool: max-active: 50max-wait: -1max-idle: 50min-idle: 1timeout: 10000

3 JedisPool 连接池配置类:

@Configuration
@PropertySource("classpath:application.yml")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-active}")private int maxActive;
@Value("${spring.redis.jedis.pool.max-idle}")private int maxIdle;
@Value("${spring.redis.jedis.pool.min-idle}")private int minIdle;
@Value("${spring.redis.jedis.pool.max-wait}")private long maxWaitMillis;
@Beanpublic JedisPool redisPoolFactory() throws Exception {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxIdle(minIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
jedisPoolConfig.setMaxTotal(maxActive);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout);return jedisPool;
}
}

4 RedisLock类:

@Componentpublic class RedisLock {private final String LOCK_MSG = "OK";private final Long UNLOCK_MSG = 1L;private final String SET_IF_NOT_EXIST = "NX";private final String SET_WITH_EXPIRE_TIME = "PX";/** * 获取锁 * @param lockKey * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */public boolean Lock(Jedis jedis,String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);if (LOCK_MSG.equals(result)) {return true;
}return false;
}/** * 阻塞获取锁 * @param lockKey * @param requestId 请求标识 * @param expireTime lockKey超时时间 * @param millisTimeout 超时获取锁时间 * @return 是否获取成功 */public boolean tryLockMillis(Jedis jedis,String lockKey, String requestId, int expireTime,int millisTimeout ) {
String result=null;long now= System.currentTimeMillis();while (System.currentTimeMillis()-now<millisTimeout){
result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);if (LOCK_MSG.equals(result)) {return true;
}
}return false;
}/** * 释放锁 * @param jedis Redis客户端 * @param lockKey * @param requestId 请求标识 * @return 是否释放成功 */public boolean unLock(Jedis jedis,String lockKey, 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(lockKey), Collections.singletonList(requestId));if (UNLOCK_MSG.equals(result)) {return true;
}return false;
}
}

5 测试类

@Slf4j
@RestControllerpublic class TestController {
@Autowiredprivate OrderService orderService;private final ExecutorService executorService = Executors.newFixedThreadPool(50);
@Autowiredprivate RedisLock redisLock;
@Autowiredprivate JedisPool jedisPool;private int num;
@RequestMapping("/testRedisLock")public String testRedisLock() throws Exception {
String lockKey="idgenerate";
String requestId = UUID.randomUUID().toString();
Jedis jedis = jedisPool.getResource();try {boolean lock = redisLock.tryLockMillis(jedis,lockKey,requestId, 1000*60,1000*1000);if(lock){
Order order = new Order();
order.setId(null);
order.setOrderId(String.valueOf(++num));
order.setOrderTime(new Date());executorService.execute(new Runnable() {
@Overridepublic void run() {orderService.insert(order);
}
});
System.out.println(num);
}
}catch (Exception e){log.info("",e);
}finally {redisLock.unLock(jedis, lockKey, requestId);
jedis.close();
}return "SUCCESS";
}
}

用jmeter测试

63b8dc4bf9ff115d88080a759832e332.png

8fff6e17c4a5e1bb93bc090eb43795d7.png
order_id加了唯一索引

00780f3c2541addf97513007ba92e05f.png

开始时间:2019-12-12 16:01:12

b8906a4d083f61abc7c8cacdb5f1786e.png

结束时间:2019-12-12 16:04:53

总共耗时3分41秒大约2毫秒保存完成一笔订单,redis性能牛逼啊。。。

下面关系加锁解锁代码分析一下

tryLockMillis超时获取锁:

lockKey: jedis.set方法参数的key

requestId: jedis.set方法参数的value 必须保证唯一,这里测试是用的uuid保证唯一性,解锁的时候需要根据这个参数去解锁,保证当前获取锁的线下去解锁。

SET_IF_NOT_EXIST:这个参数保证只有一个线程能set成功返回ok
SET_WITH_EXPIRE_TIME:表示要设置超时时间

expireTime:key超时时间(毫秒),这个具体设置多久跟你的业务逻辑执行的时长有关

millisTimeout:阻塞获取锁的时间,这个具体设置多久跟你的并发量和业务逻辑执行的时长相关

jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime)这个方法是原子操作(redis保证),成功set返回ok,同一时刻只能有一个线程可以设置成功返回ok,其他线下在设置返回的不是ok

不能把这个命令拆开,先设置key然后在设置超时时间,这样不是原子操作,解锁也存在类似问题,Lock方法为非阻塞获取锁。

public boolean unLock(Jedis jedis,String lockKey, 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(lockKey), Collections.singletonList(requestId));if (UNLOCK_MSG.equals(result)) {return true;
}return false;
}

script lua脚本的意思是:根据传入的lockKey作为KEYS[1]查找value ,如果找到的value和传入的requestId(作为ARGV[1]参数)相等就返回1

完结,觉得不错,关注一下,不足不处,望指点,互相学习进步!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值