需求:
多用户评论增加楼层
现状:
使用了synchronized本地锁。本地锁已经优化,细化过后的锁
public String commitTopicReply(ArTopicReplayModel TopicReplayModel){
// 创建插入数据库需要的实体
TopicReplyRecord topicReplyRecord = new TopicReplyRecord();
topicReplyRecord.setActive_id(BigInteger.valueOf(Long.parseLong(TopicReplayModel.getActiveId())));
// 获取传入的用户手机号
String userId = TopicReplayModel.getUserId();
// 获取回复的文本内容
String content = URLDecoder.decode(TopicReplayModel.getContent());
if (content.equals("") && TopicReplayModel.getFiles_attr().equals("")) {
return null;
}
// 存储上传的图片
List<String> urlList = new ArrayList<>();
// 用分号分隔图片或文件的url地址
String[] branch = TopicReplayModel.getFiles_attr().split(";");
for (String s : branch) {
urlList.add(s);
}
// 保存到资源记录表并返回资源组id
// 插入url地址到资源记录表中
// 插入资源组id到回复记录表中
// 插入到主题讨论回复接口中
int insertCount = saveTopicReplyRecord(topicReplyRecord,TopicReplayModel);
if (insertCount < 0) {
return null;
}
return String.valueOf(insertCount);
}
private synchronized int saveTopicReplyRecord(TopicReplyRecord topicReplyRecord,ArTopicReplayModel TopicReplayModel){
if (StringUtils.isEmpty(TopicReplayModel.getReplyId())) {
String key=CommonConfigurationUtils.NUM_OF_FLOORS+topicReplyRecord.getActive_id();
// 获取楼层数,本身increment就是原子性的
Long fool = redisTemplate.opsForValue().increment(key, 1);
topicReplyRecord.setReply_floor("第" + fool + "楼");
}
return topicReplyRecordMapper.insertTopicReplyRecord(topicReplyRecord);
}
解决办法:
使用分布式锁,Redisson的解决方案
使用步骤
Redisson:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
1.引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.15.5</version>
</dependency>
2.创建配置类
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Redisson的配置类
*
* @author Promsing(张有博)
* @version 1.0.0
* @since 2022/9/17 - 8:46
*/
@Configuration
public class MyRedisConfig {
/**
* 对redisson的使用通过RedissonClient对象
*/
@Bean
public RedissonClient redisson(){
//创建配置
Config config = new Config();
//集群模式
//config.useClusterServers().addNodeAddress("127.0.0.1:6379","127.0.0.1:6380");
//单机模式
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
config.useSingleServer().setDatabase(1);
//config.useSingleServer().setUsername().setPassword();//账号密码
//config.useSingleServer().setConnectionMinimumIdleSize()//连接池最小空闲连接数
//config.useSingleServer().setConnectionPoolSize()//连接池最大线程数
//等等
return Redisson.create(config);
/* public static RedissonClient create() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return create(config);
}*/
}
}
3.使用lock.lock() lock.ulock()
//测试可重入
private void entry(){
RLock lock = redissonClient.getLock("lock-park");
lock.lock();
System.out.println("可重入");
//lock.unlock();
}
4.分析:
使用默认的lock方法,不但会加锁,还支持可重入与看门狗机制
监控锁的看门狗超时时间单位为毫秒。该参数只适用于分布式锁的加锁请求中未明确使用leaseTimeout参数的情况。
如果该看门口未使用lockWatchdogTimeout去重新调整一个分布式锁的lockWatchdogTimeout超时,那么这个锁将变为失效状态。这个参数可以用来避免由Redisson客户端节点宕机或其他原因造成死锁的情况。
验证可重入:
redisson 中的锁,底层是一个HASH结构。uuid为key ,value为重入次数
验证自动续期:
只要占锁成功,就会启动一个定时任务:每隔 10 秒重新给锁设置过期的时间,过期时间为 30 秒。
部分源码解析
加锁时:Lua脚本
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return this.evalWriteAsync(this.getRawName(),
LongCodec.INSTANCE, command,
"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]);",
Collections.singletonList(this.getRawName()),
new Object[]{
unit.toMillis(leaseTime),
this.getLockName(threadId)
}
);
}
异步任务定时器
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture ttlRemainingFuture;
if (leaseTime != -1L) {
//加锁
ttlRemainingFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
} else {
ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
}
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e == null) {
if (ttlRemaining) {
if (leaseTime != -1L) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
//起一个定时计时器
this.scheduleExpirationRenewal(threadId);
}
}
}
});
return ttlRemainingFuture;
}
protected void scheduleExpirationRenewal(long threadId) {
RedissonBaseLock.ExpirationEntry entry = new RedissonBaseLock.ExpirationEntry();
RedissonBaseLock.ExpirationEntry oldEntry = (RedissonBaseLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
this.renewExpiration();
}
}
private void renewExpiration() {
RedissonBaseLock.ExpirationEntry ee = (RedissonBaseLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (ee != null) {
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
RedissonBaseLock.ExpirationEntry ent = (RedissonBaseLock.ExpirationEntry)RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
if (ent != null) {
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
RFuture<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
RedissonBaseLock.log.error("Can't update lock " + RedissonBaseLock.this.getRawName() + " expiration", e);
RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
} else {
if (res) {
RedissonBaseLock.this.renewExpiration();
}
}
});
}
}
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}
解锁时:Lua脚本
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return this.evalWriteAsync(this.getRawName(),
LongCodec.INSTANCE,
RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;",
Arrays.asList(this.getRawName(), this.getChannelName()),
new Object[]{
LockPubSub.UNLOCK_MESSAGE,
this.internalLockLeaseTime,
this.getLockName(threadId)});
}
完整测试代码
/**
* 简单加锁过程
*
* @return
*/
@GetMapping("floor")
public String floor() {
Long increment = null;
//只要占锁成功,就会启动一个定时任务:每隔 10 秒重新给锁设置过期的时间,过期时间为 30 秒。
/* LockSupport.park();
LockSupport.unpark(null);*/
//获取锁
RLock lock = redissonClient.getLock("lock-park");
Object floor = null;
try {
//2.加锁,默认30s
//lock.lock(2, TimeUnit.MINUTES);
lock.lock();
System.out.println("加锁成功,线程ID" + Thread.currentThread().getId());
System.out.println("加锁成功,线程Name" + Thread.currentThread().getName());
Random random = new Random();
final int i = random.nextInt(list.size());
//自增:incrby
//自减:decrby
//此方法会先检查key是否存在,存在+1,不存在先初始化,再+1
floor = list.get(0);
increment = redisTemplate.opsForValue().increment(floor, 1);
/* for (int j = 0; j < 10; j++) {
entry();
}*/
Thread.sleep(500);//休眠30s
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("解锁成功,线程Name" + Thread.currentThread().getName());
}
System.out.println("楼层" + floor + "已经加到" + increment.toString());
return increment.toString();
}
//测试可重入
private void entry() {
RLock lock = redissonClient.getLock("lock-park");
lock.lock();
//System.out.println("可重入");
lock.unlock();
}
//测试看门狗机制