摘要:Spring Boot基于redis分布式锁模拟秒杀场景,未完待续
§前言
在Java中,关于锁我想大家都很熟悉,例如synchronized和Lock等。在并发编程中,我们通过加锁来保证数据一致。但是Java中的锁,只能保证在同一个JVM进程内中执行,如果在分布式集群环境下,就缴械投降,那如何处理呢?使用Redis锁来处理。
测试用例所用软件开发环境如下:
♦ java version 13.0.1
♦ IntelliJ IDEA 2019.3.2 (Ultimate Edition)
♦ Spring Boot 2.3.0.RELEASE
♦Redis 5.0.10(暂时使用单台部署,后面使用集群)
§案例分析
模拟一个比较常见的秒杀场景,假如只有1000件商品上架,这时候就需要用到锁。在《Spring Boot 整合Jedis连接Redis和简单使用》的JedisUtil基础上增加加锁和解锁函数:
package com.eg.wiener.utils;
import com.eg.wiener.config.JedisPoolFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
import java.util.Map;
@Component
public class JedisUtil {
private static Logger logger = LoggerFactory.getLogger(JedisUtil.class);
private static String lock_key = "lock_"; //锁键
private static String lock_ok = "OK"; //锁键
protected static long internalLockLeaseTime = 30000;//锁过期时间
private long timeout = 999999; //获取锁的超时时间
private static String lua_script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//SET命令的参数
private final static SetParams params = SetParams.setParams().nx().px(internalLockLeaseTime);
@Autowired
private JedisPool jedisPool;
@Autowired
private JedisPoolFactory jedisPoolFactory;
/**
* 存储字符串键值对,永久有效
* @param key
* @param value
* @return
* @author hw
* @date 2018年12月14日
*/
public String set(String key, String value) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.set(key, value);
} catch (Exception e) {
return "-1";
} finally {
jedis.close();
}
}
/**
* 根据传入key获取指定Value
* @param key
* @return
* @author hw
* @date 2018年12月14日
*/
public String get(String key) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.get(key);
} catch (Exception e) {
return "-1";
} finally {
jedis.close();
}
}
/**
* 删除字符串键值对
* @param key
* @return
* @author hw
* @date 2018年12月14日
*/
public Long del(String key) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.del(key);
} catch (Exception e) {
return -1L;
} finally {
jedis.close();
}
}
/**
* 校验Key值是否存在
*/
public Boolean exists(String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.exists(key);
} catch (Exception e) {
return false;
} finally {
jedis.close();
}
}
/**
* 分布式锁
* @param key
* @param value
* @param time 锁的超时时间,单位:秒
*
* @return 获取锁成功返回"OK",失败返回null
*/
public String getDistributedLock(String key,String value,int time){
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.set(key, value, new SetParams().nx().ex(time));
} catch (Exception e) {
return null;
} finally {
jedis.close();
}
}
/**
* 加锁
*
* @param business_code 业务编码
* @param id
* @return
*/
public boolean lock(String business_code, String id) {
Jedis jedis = jedisPool.getResource();
Long start = System.currentTimeMillis();
try {
while (true) {
//SET命令返回OK ,则证明获取锁成功
String lock = jedis.set(lock_key.concat(business_code), id, params);
if (lock_ok.equals(lock)) {
return true;
}
//否则循环等待,在timeout时间内仍未获取到锁,则获取失败
long waitTime = System.currentTimeMillis() - start;
if (waitTime >= timeout) {
return false;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
logger.error("sleep失败,", e);
}
}
} finally {
jedis.close();
}
}
/**
* 解锁
*
* @param id
* @return
*/
public boolean unlock(String business_code, String id) {
Jedis jedis = jedisPool.getResource();
try {
Object result = jedis.eval(lua_script,
Collections.singletonList(lock_key.concat(business_code)),
Collections.singletonList(id));
if ("1".equals(result.toString())) {
return true;
}
return false;
} finally {
jedis.close();
}
}
public Map<String, String> getMap(String key) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.hgetAll(key);
} catch (Exception e) {
return null;
} finally {
jedis.close();
}
}
public Long setMap(String key, Map<String, String> value) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.hset(key, value);
} catch (Exception e) {
logger.info("-------向Redis存入Map失败--------", e);
return -1L;
} finally {
jedis.close();
}
}
}
解锁是通过jedis.eval来执行一段LUA来实现的,这是当下流行的靠谱方案,它把锁的Key键和生成的字符串当做参数传入。
在UserController中添加测试函数,模拟直播秒杀场景,上架商品数量右主播设置。由于是模拟是否加锁解锁成功,为了简化,故在程序中自动生成客户id。
private int count = 0;
/**
* 模拟直播秒杀场景,上架商品数量限指定件
*
* @param productNum 上架商品数量
* @return
* @throws InterruptedException
*/
@ApiOperation(value = "测试Redis分布式锁")
@GetMapping("/testRedisLock")
@ResponseBody
public String testRedisLock(Integer productNum) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(productNum);
ExecutorService executorService = Executors.newFixedThreadPool(100);
long start = System.currentTimeMillis();
for (int i = 0; i < productNum; i++) {
executorService.execute(() -> {
String businessCode = "test";
// 模拟下单用户,其实作为入参,为了简化,故自动生成
String id = UUID.randomUUID().toString();
try {
// jedisUtil.lock(businessCode, id);
jedisUtil.getDistributedLock(businessCode, id, 1);
count++;
} finally {
jedisUtil.unlock(businessCode, id);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
String ret = String.format("执行线程数:%s,总耗时:%s,count数为:%s", productNum, System.currentTimeMillis() - start, count);
logger.info(ret);
return ret;
}
由代码可知,商品售罄即止Swagger3测试效果如图所示:
大家可以试试另外一种加锁方式,你有Redis分布式锁的更佳实现方案吗?欢迎留言!
§小结
对于上述应用单节点Redis分布式锁模拟直播秒杀场景,您怎么看?欢迎参与话题互动讨论,分享你的观点和看法, 评论区留言哦,喜欢小编文章的朋友请点赞,谢谢你的参与!