在电商业务场景中,商品秒杀是一个常见的活动,由于业务线的划分,未在生产环境中实现秒杀业务,现在写了一个秒杀的demo供各位同仁参考.
技术栈 1.redis 2.mysql乐观锁
首先要创建一个redis连接 在此处我使用了单例模式 保证只有一个全局的连接
public class SingletonRedis {
private static volatile RedisConnection secooFrontRedis =null;
private SingletonRedis() {
}
public static RedisConnection getSecooFrontRedis() {
if (secooFrontRedis ==null) {
synchronized (SingletonRedis.class) {
if (secooFrontRedis ==null) {
secooFrontRedis =RedisConnectionFactory.getInstance().getRedisConnection("contentClusterRedis");
}
}
}
return secooFrontRedis;
}
}
建立完redis的连接之后 我们需要写一个redis的分布式锁
public class RedisLock {
private static final RedisConnection secooFrontRedis =SingletonRedis.getSecooFrontRedis();
private long outTime =2000;
private int expireTime =1000;
public RedisLock() {
}
public boolean lock(long time,String code){
while (System.currentTimeMillis()-time) //此处使线程阻塞 ,循环判断 获取锁
Long setnx =secooFrontRedis.setnx(code, "1");
if (setnx==1){
secooFrontRedis.expire(code,expireTime);
return true;
}
}
return false;
}
public void unlock(String code){
secooFrontRedis.del(code);
}
}
通过junit进行单元测试 创建1000个线程来模拟并发
public class MyJunitTest extends JunitTest {
private static final String code ="iphone";
final int threadCount =1000;
@Autowired
private TestRedis testRedis;
@Test
public void test(){
long l =System.currentTimeMillis();
for (int i =0; i
new Thread(()->
testRedis.miaosha(code)
).start();
}
long l1 =System.currentTimeMillis();
System.out.println("执行时长"+String.valueOf(l1-l));
try {
Thread.sleep(1000000000); //此处为让主线程沉睡 避免多线程执行时 dataSource close
}catch (InterruptedException e ) {
e.printStackTrace();
}
}
}
剩下的部分就是写我们的实现类
@Service
public class TestRedisImpl implements TestRedis {
@Autowired
private TestRedisMapper testRedisMapper;
@Override
@Transactional
public void miaosha(String code) {
RedisLock redisLock =new RedisLock();
try {
redisLock.lock(System.currentTimeMillis(),code);
ProductEntity productEntity =testRedisMapper.queryCount(code); //查询库存剩余数量
System.out.println(productEntity.toString());
int count =productEntity.getNum();
System.out.println(count);
if (count>0){
System.out.println("执行修改");
testRedisMapper.updateCount(productEntity.getVersion());//扣减库存
}
}catch (RuntimeException e ){
e.printStackTrace();
}finally {
redisLock.unlock(code); //释放锁
}
}
}
<update id="updateCount">
UPDATE product SET num = num-1 ,version=version+1
WHERE code ='iphone' and version = #{version} //使用数据库乐观锁 当版本号一致时 扣减库存
<select id="queryCount" resultType="com.secoo.content.api.entity.ProductEntity">
SELECT* FROM product WHERE code ='iphone'
</select>
一个demo级的秒杀代码就这样写完了 未出现超卖的情况 当然 在实际的业务场景中 秒杀涉及到的业务还有很多 比如 未支付或支付超时如何释放库存 比如 分布式系统架构中 如何处理数据一致性的问题等等
在昨天分享的部分 由于redis的setnx和expire操作是两个操作 不能保证原子性 一但setNx(key,vlue)后 服务挂掉了 那么这个key 就变成了一个永久的锁 在生产环境中是不允许的所以我们就要用其他的方式来保证操作的原子性
1.redis +lua
private static final String LUA ="if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end";
Listkeys =new ArrayList<>();
keys.add(code);
Listargs =new ArrayList<>();
args.add("1");
args.add("1000");
while (System.currentTimeMillis() - time
Object eval =secooFrontRedis.eval(LUA, keys, args);
String s =eval.toString();
if (s.equals("1")) {
return true;
}
2 redis 2.6.12版本后的本身就实现了setNx的原子操作
@Override
public String set(String key, String value, Boolean isNx, long pxMillSeconds) {
return redisProxy.set(key, value, isNx, pxMillSeconds);
}
while (System.currentTimeMillis() - time
String set =secooFrontRedis.set(code, "1", true, 1000);
if (StringUtils.isNotBlank(set) &&set.equals("OK")) {
return true;
}
}
代码部分其实很简单 但是到这里并没有结束 我们设想一个场景
有两个线程 第一个线程进去 在执行的时候阻塞了 过了失效时间 这时第二个线程进入 执行完 删了锁 那么删除的这个锁其实是第一个线程的
要如何避免这种情况 可能我们就需要设置一个守护线程 为redis的锁去延长失效时间