平时我们在写代码的时候,通常都会这么去写,这种在普通的管理系统中是OK的,但是到并发高的情景下,就会出现问题,这里以jmeter压测为例就会出现问题。
public final static String REDIS_LOCK = "redis_lock";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@GetMapping("/test")
public void test() {
String num = stringRedisTemplate.opsForValue().get(REDIS_VALUE);
assert num != null;
int value = Integer.parseInt(num);
if (value > 0) {
Long newNum = stringRedisTemplate.opsForValue().decrement(REDIS_VALUE);
stringRedisTemplate.opsForValue().set(REDIS_VALUE, String.valueOf(newNum));
System.out.println("库存还剩" + newNum + "件" + ",目前访问的节点是" + port);
} else {
System.out.println("卖完了!");
}
}
- 在单节点应用中,我们可以加上
JVM提供的内置锁(synchronize、reentrantLock)等操作,即可以解决这个问题。
synchronized (this) {
if (value > 0) {
// 业务代码
}
}
-
如果将上面的程序部署两份,通过
Nginx去调用,就算是加了内置锁,也是会出现问题的。 -
这个时候就要用分布式锁了,这里用Redis实现分布式锁,先自己实现。
public final static String REDIS_LOCK = "redis_lock";
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, REDIS_LOCK);
if (!flag) {
System.out.println("锁已经被占用!");
return;
}
try {
// 业务代码
} finally {
stringRedisTemplate.delete(REDIS_LOCK);
}
上面这种会有锁不释放的问题,可能服务挂了,程序还没执行到
finally就挂了,这个时候就会出现锁不释放的问题
- 解决上面的可以加上过期时间
注意添加过期时间和设置值的原子性
// 这个是保证原子性的
String redisValue = UUID.randomUUID().toString() + Thread.currentThread().getName().toString();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, redisValue, 10L, TimeUnit.SECONDS);
// 下面这两个是不具备原子性的
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, redisValue);
stringRedisTemplate.expire(REDIS_LOCK,10L,TimeUnit.SECONDS);
- 就算是设置了过期时间,也还可能出现问题,当执行业务的时间超过设置的过期时间,这个时候业务代码还没有执行完,
Redis就自动把锁给释放了,这个时候其他的线程就会跑进来抢到锁,而此时前面一个线程执行完了,会执行finally里面的删除锁操作,这个时候就会把别人的锁删除掉,通过下面的方式可以解决。
if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(redisValue)) {
stringRedisTemplate.delete(REDIS_LOCK);
}
- 但是删除锁的操作也要要求原子性,可以采用官方提供的lua脚本操作
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
jedis.eval(script, Arrays.asList(REDIS_LOCK), Arrays.asList(REDIS_VALUE));
- 也可以通过
Redis事务的方式实现,只是这种基本上不常用,应付面试
if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(redisValue)) {
for (; ; ) {
// 监听某一个key,当被其他线程修改以后,就会退出然后重试
stringRedisTemplate.watch(REDIS_LOCK);
// 开启事务
stringRedisTemplate.setEnableTransactionSupport(true);
stringRedisTemplate.multi();
stringRedisTemplate.delete(REDIS_LOCK);
stringRedisTemplate.exec();
}
}
-
就算把上面的过期时间变大,也无法保证业务执行时间不会超过那个过期时间,这个时候需要开启一个续命线程,动态扩展过期时间,这个时候可以考虑使用
Redisson,别人写的肯定比我们自己实现的要好,自己实现的在生产中可能会出现各种问题 -
使用Redisson实现最终分布式锁
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.14.1</version>
</dependency>
@Bean
public Redisson redisson() {
Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://172.17.81.71:31711");
return (Redisson) Redisson.create(config);
}
@RestController
public class TestController {
public final static String REDIS_VALUE = "redis_value";
public final static String REDIS_LOCK = "redis_lock";
@Autowired
Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@GetMapping("/test")
public void test() {
RLock lock = redisson.getLock(REDIS_LOCK);
lock.lock();
try {
String num = stringRedisTemplate.opsForValue().get(REDIS_VALUE);
assert num != null;
int value = Integer.parseInt(num);
if (value > 0) {
Long newNum = stringRedisTemplate.opsForValue().decrement(REDIS_VALUE);
stringRedisTemplate.opsForValue().set(REDIS_VALUE, String.valueOf(newNum));
System.out.println("库存还剩" + newNum + "件" + ",目前访问的节点是" + port);
} else {
System.out.println("卖完了!");
}
} finally {
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}