前言:隨著互聯網的發展,單體web應用已無法滿足業務的需求,隨之而來的是微服務,再加上分布式部署,帶來的是各種問題,個個服務在不同的行程,當對同一資源進行修改時就會發生執行緒安全問題,特別是在電商活動(搶優惠券、下單等業務場景),記錄一下自己探索分布式鎖的程序
一、單機redis下基于jedis分布式鎖
1、環境: redis-server: redis-6.0.3 、 redis-client: jedis 2.9.1、springboot :2.1.3
2、創建redis連接池
public class RedisPool {
private static JedisPool pool;//jedis連接池
private static Jedis jedis;
private static int maxTotal = 20;//最大連接數
private static int maxIdle = 10;//最大空閑連接數
private static int minIdle = 5;//最小空閑連接數
private static boolean testOnBorrow = true;//在取連接時測驗連接的可用性
private static boolean testOnReturn = false;//再還連接時不測驗連接的可用性
static {
initPool();//初始化連接池
}
public static Jedis getJedis(){
return pool.getResource();
}
public static void close(Jedis jedis){
jedis.close();
}
private static void initPool(){
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(maxTotal);
config.setMaxIdle(maxIdle);
config.setMinIdle(minIdle);
config.setTestOnBorrow(testOnBorrow);
config.setTestOnReturn(testOnReturn);
config.setBlockWhenExhausted(true);
pool = new JedisPool(config, "192.168.106.120", 6379, 5000, "redis");
}
}
3、創建RedisClient,實作分布式鎖工具類
public class RedisClient {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final Long RELEASE_SUCCESS = 1L;
/**
* 嘗試獲取分布式鎖,該方法確保了在同一時刻只有一個執行緒能搶占到鎖
* @param lockKey 鎖
* @param requestId 請求標識
* @param expireTime 超期時間
* @return 是否獲取成功
*/
public boolean tryGetLock( String lockKey, String requestId, int expireTime) {
Jedis jedis = RedisPool.getJedis();
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
RedisPool.close(jedis);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
/**
* 釋放分布式鎖
* @param lockKey 鎖
* @param requestId 請求標識
* @return 是否釋放成功
*/
public boolean releaseLock( String lockKey, String requestId) {
Jedis jedis = RedisPool.getJedis();
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey),
Collections.singletonList(requestId));
RedisPool.close(jedis);
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
/**
* 設定key值
* @param key
* @param value
* @return
*/
public boolean setKey( String key, String value) {
Jedis jedis = RedisPool.getJedis();
String result = jedis.set(key.getBytes(), value.getBytes());
RedisPool.close(jedis);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
/**
* 獲取key操作
* @param key
* @return
*/
public String getKey( String key) {
Jedis jedis = RedisPool.getJedis();
String result = jedis.get(key);
RedisPool.close(jedis);
return result;
}
}
4、裝配RedisClient到spring ioc容器
@Configuration
public class RedisClientConf {
@Bean
public RedisClient redisClient() {
return new RedisClient();
}
}
5 、撰寫service,用CountDownLatch結合ExecutorService模擬并發測驗分布式鎖
@Service
@SuppressWarnings("all")
public class RedisService {
// 商品鎖 key 值
private String lockKey = "computer_key";
private Logger logger = LoggerFactory.getLogger(RedisService.class);
// 執行緒池
ExecutorService executorService = new ThreadPoolExecutor(4, 4, 1L,
TimeUnit.MICROSECONDS, new LinkedBlockingDeque<Runnable>());
@Autowired
private RedisClient redisClient;
public List<String> testRedisLock() {
CountDownLatch countDownLatch = new CountDownLatch(200);
// 搶到商品的用戶
List<String> shopUser = new ArrayList<>();
// 模擬用戶資料
List<String> userArray = new ArrayList<>();
for (int i = 0; i < 200; i++) {
userArray.add(UUID.randomUUID().toString());
}
// 模擬搶單
userArray.stream().parallel().forEach(userId -> {
executorService.execute(() -> {
String user = takeOrder(userId);
if (!StringUtils.isEmpty(user)) {
shopUser.add(user);
}
countDownLatch.countDown();
});
});
// executorService.shutdown(); shutdown后,后續再呼叫會觸發決絕策略
System.out.println("wait start");
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait end ! 成功搶單用戶數量:" + shopUser.size());
return shopUser;
}
private String takeOrder(String b) {
while (true) {
if (redisClient.tryGetLock(lockKey, b, 500)) { // 設定鎖的過期時間,避免宕機或者其他情況,導致死鎖
// cumputer_stock 為redis中提前設定好的庫存
String stockStr = redisClient.getKey("cumputer_stock");
int stock = Integer.parseInt(stockStr);
logger.info("用戶:{} 獲取鎖", b);
try {
if (stock <= 0) {// 檢查庫存
logger.info("已售罄");
break;
}
try {
// 模擬業務操作
Thread.sleep(new Random().nextInt(300));
} catch (InterruptedException e) {
e.printStackTrace();
}
// 扣減庫存
redisClient.setKey("cumputer_stock", (stock - 1) + "");
logger.info("用戶:{},搶單成功, 剩余庫存: {}", b, stock -1);
} finally {
// 釋放鎖, 必須放在finally,確保鎖能釋放
if (b.equals(redisClient.getKey(lockKey))) { // 避免誤刪,導致鎖失效
boolean flag = redisClient.releaseLock(lockKey, b.toString());
logger.info("用戶:{},釋放鎖: {}", b, flag);
}
}
return b;
}
}
return null;
}
}
6、測驗分布式鎖效果:初始化cumputer_stock 庫存為100,啟動多個應用訪問介面,在查看結果

測驗結果:
8080埠下:成功搶單用戶數量:41
8081埠下:成功搶單用戶數量:30
8082埠下:成功搶單用戶數量:29
結論:經過多次并發測驗,不會導致超賣現象,單機下redis分布式鎖已成功完成(這里還有一個問題,就是程式無法判斷業務代碼的執行時間,超時時間設定多少都不合適,解決方案是開一個子執行緒來為延長redis key 的超時時間,以后成熟的框架,后面會見到Redission,來實作分布式鎖會很簡單)
二、 單機redis下基于Spring Data Redis分布式鎖
1、環境: redis-server: redis-6.0.3 ,pom檔案引入spring-boot-starter-data-redis
2、模擬并發測驗代碼,測驗結果一樣,只不過是更簡單,只需要在組態檔中配置redis的資訊即可
@Service
@SuppressWarnings("all")
public class SpringDataRedisLock {
沈略部分代碼,和上面service代碼一樣,只不過是把操作redis的工具類換成了RtringRedisTemplate
private String takeOrder(String b) {
while (true) {
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, b, 500, TimeUnit.MICROSECONDS);
if (lockFlag) { // 設定鎖的過期時間,避免宕機或者其他情況,導致死鎖
// cumputer_stock 為redis中提前設定好的庫存
String stockStr = stringRedisTemplate.opsForValue().get("cumputer_stock");
int stock = Integer.parseInt(stockStr);
logger.info("用戶:{} 獲取鎖", b);
try {
if (stock <= 0) {// 檢查庫存
logger.info("已售罄");
break;
}
try {
// 模擬業務操作
Thread.sleep(new Random().nextInt(300));
} catch (InterruptedException e) {
e.printStackTrace();
}
// 扣減庫存
stringRedisTemplate.opsForValue().set("cumputer_stock", (stock - 1) + "");
logger.info("用戶:{},搶單成功, 剩余庫存: {}", b, stock -1);
} finally {
// 釋放鎖, 必須放在finally,確保鎖能釋放
if (b.equals(stringRedisTemplate.opsForValue().get(lockKey))) { // 避免誤刪,導致鎖失效
boolean flag = stringRedisTemplate.delete(lockKey);
logger.info("用戶:{},釋放鎖: {}", b, flag);
}
}
return b;
}
}
return null;
}
}
三、初探Redission分布式鎖框架
1、pom引入相關依賴
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
2、配置RedissionClient
@Configuration
public class RedissionClientConf {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.setTransportMode(TransportMode.NIO);
config.useSingleServer().setAddress("redis://192.168.106.120:6379").setPassword("****");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
3、RedisssionClient 鎖的使用
@Service
@SuppressWarnings("all")
public class RedissionService {
// 商品鎖 key 值
private String lockKey = "computer_key";
private Logger logger = LoggerFactory.getLogger(RedissionService.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient;
public String takeOrder() {
String userID = UUID.randomUUID().toString();
RLock rLock = redissonClient.getLock(lockKey);
while (true) {
rLock.lock();
// cumputer_stock 為redis中提前設定好的庫存
String stockStr = stringRedisTemplate.opsForValue().get("cumputer_stock");
int stock = Integer.parseInt(stockStr);
logger.info("用戶:{} 獲取鎖", userID);
try {
if (stock <= 0) {// 檢查庫存
logger.info("已售罄");
break;
}
try {
// 模擬業務操作
Thread.sleep(new Random().nextInt(3000));
} catch (InterruptedException e) {
e.printStackTrace();
}
// 扣減庫存
stringRedisTemplate.opsForValue().set("cumputer_stock", (stock - 1) + "");
logger.info("用戶:{},搶單成功, 剩余庫存: {}", userID, stock - 1);
} finally {
// 釋放鎖, 必須放在finally,確保鎖能釋放
rLock.unlock();
}
return "用戶:" + userID + ",搶單成功";
}
return "已售罄";
}
}
4、撰寫Controller,用jemeter對介面進行壓測,設定500并發,redis中配置100庫存,測驗結果如下,發現也不會出現超賣現象,陳序運行穩定,

至此對redis單機環境下分布式鎖算是入門了,歡迎指正,
文章詳情請查看(redis集群環境下分布式鎖解決方案): http://www.xiaoyuge.com.cn/#/article/detail?articleId=64
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/12983.html
標籤:其他
上一篇:迭代器模式在開源代碼中的應用
