数据设计:
num:offset(offset是zet里的分数),skuID%50是sku所在的zset,会有线程定时消费zset里的数据,采用Lua脚本保证原子性:
lua步骤:
1.查询sku,版本号,版本号大于zset标识已经消费过了,跳过消费
2.执行更换,
3.删除zset里的数据
定时消费逻辑:
1.redis获取分布式锁,
2.从redis zset里获取数据
3.执行lua脚本回单
分布式锁因为是消费redis里的数据,所以每次执行lua脚本前都需要检测锁是否还在,不在退出执行,分布式锁设置成永久锁.watchDog定时续期
商品扣减逻辑:
由于回滚的需要,按照按照队列进行扣减.采用多线程部署的方式,设置一个timeout和一个ConcurrentMap记录扣减结果,超时就表示扣减失败;重新提交订单;
扣减库存的lua:
-- 查询商品数量,不够就不扣了
-- 查询sku对应的zset的成员分数值,对该值增1,失败就重试三次(hashmap结构,hget)
-- 更新zset分数值
-- 写zset
-- 更新sku值.
-- 查询redisId,返回
实现为:
local skuNumNeed=tonumber(ARGV[1]) or 0
local skuId=KEYS[1]
local sku_redo_log_queue=KEYS[2]
local orderItemId=ARGV[2]
-- 查询商品数量,不够就不扣了
local numStr=redis.call('hget','sku',skuId)
local key, value = string.match(numStr, "([^:]+):([^:]+)")
-- 如果值是数字,可以转换
value = tonumber(value) or value
if value < skuNumNeed then
return ""
end
-- 查询sku对应的zset的成员分数值,对该值增1,失败就重试三次(hashmap结构,hget)
local offset=redis.call('hget','queueOffset',sku_redo_log_queue)
-- 更新zset分数值
local numOffset = tonumber(offset) or 0
local newOffset=numOffset+1
redis.call('hset','queueOffset',sku_redo_log_queue,newOffset)
-- 写zset
redis.call('zadd',sku_redo_log_queue, offset,orderItemId)
--更新商品
local skuNewNum=value-skuNumNeed
local skuNew=skuId .. tostring(skuNewNum)
redis.call('hset','sku',skuId,skuNew)
-- 查询redisId,返回
local redisId=redis.call('hget','metadata','redis_id')
if redisId==nil or redisId=="" then
return ""
end
return redisId
该类用于在多线程间传递结果.
class MultiSubmitOrderWEntity{
Map<String,List<>> map 记录扣减成功的数据
AtomicInteger status; 用于在多线程间传递结果.
countDownLatch 主线程阻塞.
}
扣减主线程代码:
OrderVo order;
orderMap=new MultiSubmitOrderMap();
latch=orderMap.getLatch();
//按照redis分片的逻辑(也是mysql分片的逻辑),将商品进行分类;
List<List<OrderItem>> list=order.getList();
for int i=0;i<list.size();i++{
submit(orderMap,list.get(i)); //提交至线程池
}
latch.countDown(list.size(),500ms);
//判断扣减状态,
//失败就回滚
//成功需要写消息队列.
扣减线程代码: OrderItemStockRedisRun
for循环扣减队列里的商品
//检查是否有扣减失败的,有就终止扣减,退出
//扣减
查询数量,不够continue
lua脚本扣减
返回redisID
latch.countDown();
扣减失败就需要回滚,
local skuID=KEYS[1]
local skuNum=ARGV[1]
local numStr=redis.call('hget','sku',skuID)
local key, value = string.match(numStr, "([^:]+):([^:]+)")
-- 如果值是数字,可以转换
value = tonumber(value) or value
--更新商品
local skuNewNum=value+skuNum
local skuNew=skuId .. tostring(skuNewNum)
redis.call('hset','sku',skuId,skuNew)
return 0
商品扣减过程中路由是一个问题,不同的skuId需要路由到不同的redis上,这里使用Dubbo构建redis代理.
实际的商品扣减
超时回单:
1.mysql:利用消息队列,超时回单
2.redis:利用redis本身的队列,超时回单.
20bit 50 50 000 50 000 000
1 10 10 000 10 000 000
100bit 1k 1M 1G
10个队列,10G,管理1亿个订单项
9 223 372 036 854 775 807 17位
orderItemId: skuId: skuNum
17 17 4
从redoLog角度看只需要存储orderItemId就行了,到时都是要查数据库的,所以数据大小按照50字节来估算,10G可以存储2亿条订单项.
redis代理实现:
通过Dubbo,connectionFactory两个层面实现redis代理
1.dubbo过渡不同的业务
2.connectionFactory区分同一个业务的不同数据源
连接池的实现目前有Jedis,Lettuce两个,Lettuce的性能远好于Jedis,因此本次使用Lettuce实现多数据源
使用ConcurrentHashMap保存redisTemplate;
Map<String,Map<String,StringRedisTemplate>> redisTemplateMap=new ConcurrentHashMap();
第一个String是业务区分,第二个是ip区分,用于删除,添加redisTemplate
在某个方法里统一创建好所有的redisTemplate;使用时获取即可.




