概述:并发量由低到高,单机到集群,java对锁、分布式锁、准实时方案的概要实现;全文以商品抢购为例。
目录
正文:
lock和synchronized均可,单机;
实例:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 锁控制类
* @author Administrator
* @date 20230523
*/
@RestController
@RequestMapping("/locks")
public class LockController {
/**
* 商品库存
*/
public static int productInventoryNums = 100;
@PostMapping("/standalone")
public synchronized String standAlone(){
if(productInventoryNums > 0){
productInventoryNums--;
} else {
return "fail";
}
return "success";
}
}```
```
先结论再说原理(此处暂未涉及数据强一致性,放在原理中进行分析);
package com.xxxx.distributelocks.controller;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 商品抢购
* 并发
* @author py
* @date 20230526
*/
@RestController
@RequestMapping("/products")
public class ProductsController {
@Autowired
private Redisson redisson;
/**
* 商品剩余库存
* 从数据库或缓存中读取
*/
public static int productNums = 100;
/**
* 下单
* @return
*/
@PostMapping("/buyproduct")
public String order(String productId){
//获取锁
RLock rLock = redisson.getLock(productId);
try {
//加锁
rLock.lock();
if(productNums > 0){
productNums--;
} else {
//商品已售罄
return "fail";
}
//更新库存
//库存写入缓存或数据库
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
rLock.unlock();
}
return "success";
}
}
此处可对部分场景进行优化,第一种是读多写少,或许这也是大多数互联网公司采用的方案;
1)读多写少
`
/**
* 下单
* @param productId
* @return
*/
@PostMapping("/buyproduct1")
public String writeOrder(String productId){
//获取锁
RReadWriteLock rwLock = redisson.getReadWriteLock(productId);
RLock writeLock = rwLock.writeLock();
try {
//此处为写场景,故加写锁
writeLock.lock();
if(productNums > 0){
productNums--;
} else {
//商品已售罄
return "fail";
}
//更新库存
//库存写入缓存或数据库
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
writeLock.unlock();
}
return "success";
}
/**
* 读商品
* @param productId
* @return
*/
@PostMapping("/readproduct")
public String readOrder(String productId){
//获取锁
RReadWriteLock rwLock = redisson.getReadWriteLock(productId);
RLock readLock = rwLock.readLock();
try {
//此处为读场景,故加写锁
readLock.lock();
if(productNums > 0){
return productNums + "";
} else {
//商品已售罄
return "已售罄";
}
//更新库存
//库存写入缓存或数据库
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
readLock.unlock();
}
return "售罄";
}
2)读多写多场景
既然读多写也多,那还读写缓存有什么意义,直接用binlog更新缓存不香吗?
原理概述:通过setnx命令更新redis后(因为redis命令执行的单线程与数据分布式存储的特性,是分布式锁可用的前提),更新之后定时更新加锁的key(也就是业内说的锁续命逻辑);
`
RFuture tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand 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)}
);
}
其中lua脚本本身命令的原子性操作;
然后再看锁续命逻辑
`
public RFuture tryLockAsync() {
return this.tryLockAsync(Thread.currentThread().getId());
}
private RFuture tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture ttlRemainingFuture;
if (leaseTime != -1L) {
ttlRemainingFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
CompletionStage f = ttlRemainingFuture.thenApply((ttlRemaining) -> {
if (ttlRemaining == null) {
if (leaseTime != -1L) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
this.scheduleExpirationRenewal(threadId);
}
}
return ttlRemaining;
});
return new CompletableFutureWrapper(f);
}
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);
try {
this.renewExpiration();
} finally {
if (Thread.currentThread().isInterrupted()) {
this.cancelExpirationRenewal(threadId);
}
}
}
}
添加监听定时回调逻辑;
与netty中监听回调有异曲同工之妙,netty中是虽然并没有减少总处理时间,但却提高了吞吐量,将线程尽可能活跃起来;
此处暂时分析原理与优化方案,代码实例后续补充;
准实时方案,顾名思义,服务降级,在业务允许的延迟范围内进行计算,比如消息队列存储消息,storm、flink等下游消费,计算输出;
此处针对部分场景说明;
第一步:数据分流
数据分流,比如可根据用户id强制将每个用户的计算数据分到指定机器上;
第二步:降级、加锁
200K缓存或200条记录,200毫秒超时入库等读写降级;
单机可用sychronized或lock;
第三步:锁优化
客户端并发持续上升,单机扛不住,计算单机计算量,多台机器分布式锁计算,锁优化如上。
很多业务场景要求数据抗风险能力极强,比如商品交易金钱一致性等;
异常情况:如redis节点在写入数据后还未来得及同步到从节点时主节点突然宕机情况,此时从节点需晋升为主节点但却丢失锁数据;
至于解决方案,业内通常采用的有:
1)zookeeper
2)redlock
原理概要:
1)其中zookeeper集群相对redis集群不同之处在于zookeeper集群在主从同步时只有超过半数节点同步成功后才会将请求响应回客户端,并且从节点在晋升为主节点时一定是同步成功锁的节点晋升成功,而redis是主节点写入成功后则给客户端响应结果,并且从晋升主的过程中投票机制只能大概率保证数据偏移量最大节点晋升成功;
故如果对性能要求高建议redis,对数据一致性要求高建议zookeeper;
2)redlock在业内虽存在部分争议,但其实现仍可圈可点,其实现原理为采用多台完全独立的redis节点(通常为奇数),只有半数机器同步成功后才响应到客户端,此处与zookeeper原理大同小异;
因个人水平有限,时间精力有限,后续提升待完善,抛砖引玉,望大家多多提出改善之处;