package com. kongjs. mall. tcc. impl ;
import com. kongjs. mall. mapper. GoodsStockMapper ;
import com. kongjs. mall. tcc. model. dto. GoodsStockDTO ;
import com. kongjs. mall. tcc. service. GoodsStockTccService ;
import jakarta. annotation. Resource ;
import lombok. extern. slf4j. Slf4j ;
import org. apache. dubbo. config. annotation. DubboService ;
import org. apache. seata. rm. tcc. api. BusinessActionContext ;
import org. apache. seata. rm. tcc. api. LocalTCC ;
import org. redisson. api. RBucket ;
import org. redisson. api. RLock ;
import org. redisson. api. RMap ;
import org. redisson. api. RedissonClient ;
import org. springframework. core. io. ClassPathResource ;
import org. springframework. data. redis. core. StringRedisTemplate ;
import org. springframework. data. redis. core. script. DefaultRedisScript ;
import org. springframework. transaction. support. TransactionTemplate ;
import java. io. IOException ;
import java. nio. charset. Charset ;
import java. util. Arrays ;
import java. util. Collections ;
import java. util. Objects ;
import java. util. concurrent. TimeUnit ;
@Slf4j
@LocalTCC
@DubboService
public class GoodsStockTccServiceImpl implements GoodsStockTccService {
public static final int STATUS_INIT = 0 ;
public static final int STATUS_TRIED = 1 ;
public static final int STATUS_COMMITTED = 2 ;
public static final int STATUS_ROLLBACKED = 3 ;
public static final int STATUS_SUSPENDED = 4 ;
private static final String STOCK_AVAILABLE_KEY = "goods:stock:available:" ;
private static final String STOCK_FROZEN_KEY = "goods:stock:frozen:" ;
private static final String STOCK_LOCK_KEY = "goods:stock:lock:" ;
private static final long LOCK_WAIT_TIME = 3 ;
private static final long LOCK_LEASE_TIME = 10 ;
private static final long MARK_EXPIRE_TIME = 86400 ;
private static final String PREPARE_LUA_SCRIPT;
private static final String COMMIT_LUA_SCRIPT;
private static final String ROLLBACK_LUA_SCRIPT;
static {
try {
PREPARE_LUA_SCRIPT = new ClassPathResource ( "lua/goodsStockPrepare.lua" ) . getContentAsString ( Charset . defaultCharset ( ) ) ;
COMMIT_LUA_SCRIPT = new ClassPathResource ( "lua/goodsStockCommit.lua" ) . getContentAsString ( Charset . defaultCharset ( ) ) ;
ROLLBACK_LUA_SCRIPT = new ClassPathResource ( "lua/goodsStockRollback.lua" ) . getContentAsString ( Charset . defaultCharset ( ) ) ;
} catch ( IOException e) {
throw new RuntimeException ( e) ;
}
}
@Resource
private RedissonClient redissonClient;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private TransactionTemplate transactionTemplate;
@Resource
private GoodsStockMapper goodsStockMapper;
@Override
public boolean prepare ( BusinessActionContext context, GoodsStockDTO dto) {
String xid = context. getXid ( ) ;
Long goodsId = dto. getGoodsId ( ) ;
Integer reduceCount = dto. getReduceCount ( ) ;
Integer status = getStatus ( context) ;
if ( status != null ) {
if ( status == STATUS_COMMITTED || status == STATUS_ROLLBACKED) {
log. info ( "Prepare幂等处理(已完成), xid:{}, status:{}" , xid, status) ;
return true ;
} else if ( status == STATUS_TRIED) {
log. info ( "Prepare幂等处理(处理中), xid:{}" , xid) ;
return true ;
}
}
RLock lock = redissonClient. getLock ( getLockKey ( goodsId) ) ;
try {
boolean locked;
try {
locked = lock. tryLock ( LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit . SECONDS) ;
} catch ( InterruptedException e) {
Thread . currentThread ( ) . interrupt ( ) ;
return false ;
}
if ( ! locked) {
log. warn ( "获取锁失败, goodsId:{}, xid:{}" , goodsId, xid) ;
return false ;
}
transactionTemplate. executeWithoutResult ( ( transactionStatus) -> {
try {
syncPrepareToDb ( goodsId, reduceCount) ;
DefaultRedisScript < Long > script = new DefaultRedisScript < > ( PREPARE_LUA_SCRIPT, Long . class ) ;
Long result = stringRedisTemplate. execute ( script, Arrays . asList ( getAvailableKey ( goodsId) , getFrozenKey ( goodsId) ) , reduceCount. toString ( ) ) ;
if ( ! Objects . equals ( result, 1L ) ) {
throw new RuntimeException ( ) ;
}
setStatusTried ( context) ;
} catch ( Throwable e) {
transactionStatus. setRollbackOnly ( ) ;
}
} ) ;
return true ;
} finally {
if ( lock. isHeldByCurrentThread ( ) ) {
lock. unlock ( ) ;
}
}
}
@Override
public boolean commit ( BusinessActionContext context) {
String xid = context. getXid ( ) ;
GoodsStockDTO dto = context. getActionContext ( "dto" , GoodsStockDTO . class ) ;
Long goodsId = dto. getGoodsId ( ) ;
Integer reduceCount = dto. getReduceCount ( ) ;
Integer status = getStatus ( context) ;
if ( status == null ) {
return true ;
}
if ( status. equals ( STATUS_COMMITTED) ) {
return true ;
}
if ( status. equals ( STATUS_ROLLBACKED) || status. equals ( STATUS_SUSPENDED) ) {
return false ;
}
RLock lock = redissonClient. getLock ( getLockKey ( goodsId) ) ;
try {
boolean locked;
try {
locked = lock. tryLock ( LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit . SECONDS) ;
} catch ( InterruptedException e) {
Thread . currentThread ( ) . interrupt ( ) ;
return false ;
}
if ( ! locked) {
log. warn ( "提交阶段获取锁失败, goodsId:{}, xid:{}" , goodsId, xid) ;
return false ;
}
transactionTemplate. executeWithoutResult ( ( transactionStatus -> {
try {
syncCommitToDb ( goodsId, reduceCount) ;
String frozenKey = STOCK_FROZEN_KEY + goodsId;
DefaultRedisScript < Long > script = new DefaultRedisScript < > ( COMMIT_LUA_SCRIPT, Long . class ) ;
Long result = stringRedisTemplate. execute ( script, Collections . singletonList ( frozenKey) , reduceCount. toString ( ) ) ;
if ( ! Objects . equals ( result, 1L ) ) {
throw new RuntimeException ( ) ;
}
setStatusCommitted ( context) ;
} catch ( Throwable e) {
transactionStatus. setRollbackOnly ( ) ;
}
} ) ) ;
return true ;
} finally {
if ( lock. isHeldByCurrentThread ( ) ) {
lock. unlock ( ) ;
}
}
}
@Override
public boolean rollback ( BusinessActionContext context) {
String xid = context. getXid ( ) ;
GoodsStockDTO dto = context. getActionContext ( "dto" , GoodsStockDTO . class ) ;
Long goodsId = dto. getGoodsId ( ) ;
Integer reduceCount = dto. getReduceCount ( ) ;
Integer status = getStatus ( context) ;
if ( status == null ) {
return true ;
}
if ( status. equals ( STATUS_COMMITTED) ) {
return false ;
}
if ( status. equals ( STATUS_ROLLBACKED) || status. equals ( STATUS_SUSPENDED) ) {
return true ;
}
RLock lock = redissonClient. getLock ( getLockKey ( goodsId) ) ;
try {
boolean locked;
try {
locked = lock. tryLock ( LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit . SECONDS) ;
} catch ( InterruptedException e) {
Thread . currentThread ( ) . interrupt ( ) ;
return false ;
}
if ( ! locked) {
log. warn ( "回滚阶段获取锁失败, goodsId:{}, xid:{}" , goodsId, xid) ;
return false ;
}
transactionTemplate. executeWithoutResult ( transactionStatus -> {
try {
syncRollbackToDb ( goodsId, reduceCount) ;
DefaultRedisScript < Long > script = new DefaultRedisScript < > ( ROLLBACK_LUA_SCRIPT, Long . class ) ;
Long result = stringRedisTemplate. execute ( script, Arrays . asList ( getFrozenKey ( goodsId) , getAvailableKey ( goodsId) ) , reduceCount. toString ( ) ) ;
if ( ! Objects . equals ( result, 1L ) ) {
throw new RuntimeException ( ) ;
}
setStatusRollbacked ( context) ;
} catch ( Throwable e) {
transactionStatus. setRollbackOnly ( ) ;
}
} ) ;
return true ;
} finally {
if ( lock. isHeldByCurrentThread ( ) ) {
lock. unlock ( ) ;
}
}
}
private void syncPrepareToDb ( Long goodsId, Integer reduceCount) {
int i = goodsStockMapper. decreaseAvailable ( goodsId, reduceCount) ;
if ( i == 0 ) {
throw new RuntimeException ( "数据库扣减可用库存失败, goodsId:" + goodsId) ;
}
}
private void syncCommitToDb ( Long goodsId, Integer reduceCount) {
int i = goodsStockMapper. decreaseFrozen ( goodsId, reduceCount) ;
if ( i == 0 ) {
throw new RuntimeException ( "数据库扣减冻结库存失败, goodsId:" + goodsId) ;
}
}
private void syncRollbackToDb ( Long goodsId, Integer reduceCount) {
int i = goodsStockMapper. increaseAvailable ( goodsId, reduceCount) ;
if ( i == 0 ) {
throw new RuntimeException ( "数据库恢复可用库存失败, goodsId:" + goodsId) ;
}
}
private String getAvailableKey ( Long goodsId) {
return STOCK_AVAILABLE_KEY + goodsId;
}
private String getFrozenKey ( Long goodsId) {
return STOCK_FROZEN_KEY + goodsId;
}
private String getLockKey ( Long goodsId) {
return STOCK_LOCK_KEY + goodsId;
}
private RBucket < Integer > getFenceBucket ( BusinessActionContext context) {
String xid = context. getXid ( ) ;
long branchId = context. getBranchId ( ) ;
String actionName = context. getActionName ( ) ;
return redissonClient. getBucket ( "tcc:" + actionName + ":" + xid + ":" + branchId) ;
}
private Integer getStatus ( BusinessActionContext context) {
return getFenceBucket ( context) . get ( ) ;
}
private void setStatusTried ( BusinessActionContext context) {
getFenceBucket ( context) . set ( STATUS_TRIED) ;
}
private void setStatusCommitted ( BusinessActionContext context) {
getFenceBucket ( context) . set ( STATUS_COMMITTED) ;
}
private void setStatusRollbacked ( BusinessActionContext context) {
getFenceBucket ( context) . set ( STATUS_ROLLBACKED) ;
}
private void setStatusSuspended ( BusinessActionContext context) {
boolean b = getFenceBucket ( context) . setIfAbsent ( STATUS_SUSPENDED) ;
}
}
local availableKey = KEYS[ 1 ]
local frozenKey = KEYS[ 2 ]
local need = tonumber ( ARGV[ 1 ] )
local available = tonumber ( redis. call ( 'get' , availableKey) or 0 )
local frozen = tonumber ( redis. call ( 'get' , frozenKey) or 0 )
if available >= need then
redis. call ( 'decrby' , availableKey, need)
redis. call ( 'incrby' , frozenKey, need)
return 1
end
return 0
local frozenKey = KEYS[ 1 ]
local frozen = tonumber ( redis. call ( 'get' , frozenKey) or 0 )
local need = tonumber ( ARGV[ 1 ] )
if frozen >= need then
redis. call ( 'decrby' , frozenKey, need)
return 1
end
return 0
local frozenKey = KEYS[ 1 ]
local availableKey = KEYS[ 2 ]
local frozen = tonumber ( redis. call ( 'get' , frozenKey) or 0 )
local need = tonumber ( ARGV[ 1 ] )
if frozen >= need then
redis. call ( 'decrby' , frozenKey, need)
redis. call ( 'incrby' , availableKey, need)
return 1
end
return 0