分布式锁实现:
- Redis
- Redisson
- Zookeeper
本文讲解Redis和Redisson部分。
synchronized
背景:
redis中已经存储一个K/V: set stock 100
做一个减少库存的模拟。
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 修改配置文件application.properties:
server.port=8080
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.timeout=5000
#最大可用连接数(默认为8,负数表示无限)
spring.redis.jedis.pool.max-active=8
- 使用synchronized实现库存减少
public void deduct() {
synchronized (this){
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int cu = stock - 1;
redisTemplate.opsForValue().set("stock", cu + "");
System.out.println("减少库存至:" + cu);
} else {
System.out.println("库存为负数了:");
}
}
}
问题:synchronized只是在一个JVM中保持一次性只有一个线程能访问成功,但是对于多个Tomcat实例而言(多JVM/多进程),就会产生错误。
使用redis实现分布式锁!!!!
Redis
public void deduct1() {
String lockName = "product_01";
String uuid = UUID.randomUUID().toString();
try {
//定时 1/3 * timeout,续命 timeout
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockName, uuid, 10, TimeUnit.SECONDS);//10秒过期 stenx
if (!result) return;
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int cuStock = stock - 1;
redisTemplate.opsForValue().set("stock", cuStock + "");
System.out.println("减少库存至:" + cuStock);
} else {
System.out.println("库存为负数了:");
}
} finally {
//误删除别人的锁
if (uuid.equals(redisTemplate.opsForValue().get(lockName))) {
redisTemplate.delete(lockName);//释放锁
}
}
}
注意:
- 释放锁之前需要判断当前操作对象是否是加索对象,以防误删别人的锁。
- 还需要一个定时专门去对锁的超时时间进行续命。一般定时时间设置为1/3*timeout。(具体请百度)
Redisson
- 引入Redisson依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.1</version>
</dependency>
- 主类中加入:
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
- 调用:
public void deduct2() {
String lockName = "product_02";
RLock redissonLock = redisson.getLock(lockName);
try {
redissonLock.lock(10, TimeUnit.SECONDS);
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int cu = stock - 1;
redisTemplate.opsForValue().set("stock", cu + "");
System.out.println("减少库存至:" + cu);
} else {
System.out.println("库存为负数了:");
}
} finally {
redissonLock.unlock();
}
}
说明:
- lock中,底层搭配了LUA脚本使用
if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', 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]);
- 上面这条脚本在redis中操作是原子的。
- 对于主从式Redis来说,同样有问题,如果主Redis已经获取到锁,但是,此时主Redis此时宕机,从Redis则会丢失。此时又重新申请加锁则会成功!!!解决办法:
3.1 使用Redisson的RedLock 超过半数Redis节点加锁成功才算加锁成功。性能低,其他失败需要回滚成功的case。
3.2 使用Zookeeper,性能不行 需要同步到slave才会返回结果,所以能保证下一个master会存在这个锁。
以上2种都不推荐使用。
对比Redis和Redisson使用
两种方式的操作其实是一样的。优先使用Redisson,代码更加简洁。
调用虚拟机/远程Redis
以上代码中127.0.0.1 改为 IP地址即可
远程Redis配置:
>> vi etc/redis.conf
bind 127.0.0.1 192.168.X.XXX
daemonize yes
redis目录下:
>> bin/redis-server etc/redis.conf
>> redis-cli -c -h 192.168.X.XXX -p 6379
>> ps -ef | grep redis