首先介紹以下鎖,以下面減少庫存案例講解
普通情況
代碼:
public class Stock {
//庫存數量
private static int num=1;
public boolean reduceStock(){
if(num>0){
num--;
return true;
}else{
return false;
}
}
}
public class StockMain implements Runnable {
public void run() {
boolean b=new Stock().reduceStock();
if(b){
System.out.println(Thread.currentThread().getName()+"減少庫存成功!");
}else{
System.out.println(Thread.currentThread().getName()+"減少庫存失敗!");
}
}
public static void main(String []args){
new Thread(new StockMain(),"線程1").start();
new Thread(new StockMain(),"線程2").start();
}
}
結果:誰先搶到誰成功,也有可能前一個搶到沒執行完畢,後一個線程也進入判斷
,,
線程安全問題
public class Stock {
//庫存數量
private static int num=1;
public boolean reduceStock() throws InterruptedException {
if(num>0){
Thread.sleep(1000);
num--;
return true;
}else{
return false;
}
}
}
public class StockMain implements Runnable {
public void run() {
boolean b= false;
try {
b = new Stock().reduceStock();
} catch (InterruptedException e) {
e.printStackTrace();
}
if(b){
System.out.println(Thread.currentThread().getName()+"減少庫存成功!");
}else{
System.out.println(Thread.currentThread().getName()+"減少庫存失敗!");
}
}
public static void main(String []args){
new Thread(new StockMain(),"線程1").start();
new Thread(new StockMain(),"線程2").start();
}
}
結果:先搶到線程的進入方法後,沉睡一秒,足夠後面的線程進入判斷條件,所以全部成功,產生線程安全問題
加入鎖機制
public class Stock {
//庫存數量
private static int num=1;
public boolean reduceStock() throws InterruptedException {
if(num>0){
Thread.sleep(1000);
num--;
return true;
}else{
return false;
}
}
}
public class StockMain implements Runnable {
private static Lock lock=new ReentrantLock();
public void run() {
boolean b= false;
try {
//上鎖
lock.lock();;
b = new Stock().reduceStock();
//解鎖
lock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
if(b){
System.out.println(Thread.currentThread().getName()+"減少庫存成功!");
}else{
System.out.println(Thread.currentThread().getName()+"減少庫存失敗!");
}
}
public static void main(String []args){
new Thread(new StockMain(),"線程1").start();
new Thread(new StockMain(),"線程2").start();
}
}
結果:只有前一個線程執行完畢後,後一個纔會執行
引出問題:分佈式系統集羣環境下,負載均衡之後,服務不可能只發給一臺機器,鎖機制已經不滿足現狀,分佈式鎖出現。
分佈式鎖
1.數據庫實現分佈式鎖的思路分析(操作同一數據庫)
數據庫分佈式鎖實現
創建表
CREATE TABLE `lock_record` (
`id` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`lock_name` VARCHAR ( 50 ) DEFAULT NULL COMMENT '鎖名稱',
PRIMARY KEY ( `id` ),
UNIQUE KEY `lock_name` ( `lock_name` )
) ENGINE = INNODB AUTO_INCREMENT = 38 DEFAULT CHARSET = utf8
定義鎖
實現Lock接口,tryLock()嘗試獲取鎖,從鎖表中查詢指定的鎖記 錄,如果查詢到記錄,說明 已經上鎖,不能再上鎖
上鎖
在lock方法獲取鎖之前先調用tryLock()方法嘗試獲取鎖,如果未加鎖則向鎖表中插入一條鎖記錄來獲取 鎖,這裏我們通過循環,如果上鎖我們一致等待鎖的釋放
釋放鎖
即是將數據庫中對應的鎖表記錄刪除
import com.itheima.demo.bean.LockRecord;
import com.itheima.demo.mapper.LockRecordMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import tk.mybatis.mapper.entity.Example;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
@Component
public class DbLock implements Lock {
private static final String LOCK_NAME = "db_lock_stock";
@Autowired
private LockRecordMapper lockRecordMapper;
//上鎖
@Override
public void lock() {
while(true){
if(tryLock()){
//向鎖表中插入一條記錄
LockRecord lockRecord = new LockRecord();
lockRecord.setLockName(LOCK_NAME);
lockRecordMapper.insertSelective(lockRecord);
return;
}else{
System.out.println("等待鎖.......");
}
}
}
//嘗試獲取鎖
@Override
public boolean tryLock() {
//查詢lockRecord的記錄
Example example = new Example(LockRecord.class);
Example.Criteria criteria = example.createCriteria();
criteria.andEqualTo("lockName",LOCK_NAME);
LockRecord lockRecord = lockRecordMapper.selectOneByExample(example);
if(lockRecord==null){
return true;
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
//釋放鎖的操作
@Override
public void unlock() {
Example example = new Example(LockRecord.class);
Example.Criteria criteria = example.createCriteria();
criteria.andEqualTo("lockName",LOCK_NAME);
lockRecordMapper.deleteByExample(example);
}
@Override
public Condition newCondition() {
return null;
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
}
2.redis實現分佈式鎖
redis分佈式鎖的實現基於setnx(set if not exists),設置成功,返回1;設置失敗,返回0,
釋放鎖的操作通過del指令來完成 如果設置鎖後在執行中間過程時,程序拋出異常,導致del指令沒有調用,鎖永遠無法釋放,這樣就會 陷入死鎖。所以我們拿到鎖之後會給鎖加上一個過期時間,這樣即使中間出現異常,過期時間到後會自動釋放鎖。同時在setnx 和 expire 如果進程掛掉,expire不能執行也會死鎖。所以要保證setnx和expire是一個原子性操作即可。
redis 2.8之後推出了setnx和expire的組合指令
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
@Component
public class RedisLock implements Lock {
private static final String LOCK_NAME = "redis_stock_lock";
@Autowired
private RedisTemplate redisTemplate;
@Override
public void lock() {
while(true){
//上鎖 setnx
// Boolean isLock = redisTemplate.opsForValue().setIfAbsent("lockName", LOCK_NAME);
Boolean isLock = redisTemplate.opsForValue().setIfAbsent("lockName",LOCK_NAME,10,TimeUnit.SECONDS);
//思考 鎖過期怎麼辦?如何保證鎖不過期 鎖的自動續期 20 18 20 20
if(isLock){
return;
}else{
System.out.println("等待鎖........");
}
}
}
@Override
public void unlock() {
// 刪除指定的鎖的key
redisTemplate.delete("lockName");
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public Condition newCondition() {
return null;
}
}
分佈式鎖的redisson實現:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
獲取鎖,釋放鎖
Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDataba se(0);
Redisson redisson = (Redisson) Redisson.create(config);
RLock mylock = redisson.getLock(key);
//獲取鎖
mylock.lock();
//釋放鎖
mylock.unlock();
3.zookeeper實現分佈式鎖
zookeeper通過創建臨時序列節點來實現分佈式鎖,適用於順序執行的程序,大體思路就是創建 臨時序列節點,找出最小的序列節點,獲取分佈式鎖,程序執行完成之後此序列節點消失,通過watch 來監控節點的變化,從剩下的節點的找到最小的序列節點,獲取分佈式鎖,執行相應處理,依次類推
釋放鎖
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class ZkLock implements Lock {
//zk客戶端
private ZooKeeper zk;
//zk是一個目錄結構,locks
private String root = "/locks";
//鎖的名稱
private String lockName;
//當前線程創建的序列node
private ThreadLocal<String> nodeId = new ThreadLocal<>();
//用來同步等待zkclient鏈接到了服務端
private CountDownLatch connectedSignal = new CountDownLatch(1);
private final static int sessionTimeout = 3000;
private final static byte[] data= new byte[0];
public ZkLock(String config, String lockName) {
this.lockName = lockName;
try {
zk = new ZooKeeper(config, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
// 建立連接
if (event.getState() == Event.KeeperState.SyncConnected) {
connectedSignal.countDown();
}
}
});
connectedSignal.await();
Stat stat = zk.exists(root, false);
if (null == stat) {
// 創建根節點
zk.create(root, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
class LockWatcher implements Watcher {
private CountDownLatch latch = null;
public LockWatcher(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDeleted)
latch.countDown();
}
}
@Override
public void lock() {
try {
// 創建臨時子節點
String myNode = zk.create(root + "/" + lockName , data, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(Thread.currentThread().getName()+myNode+ "created");
// 取出所有子節點
List<String> subNodes = zk.getChildren(root, false);
TreeSet<String> sortedNodes = new TreeSet<>();
for(String node :subNodes) {
sortedNodes.add(root +"/" +node);
}
String smallNode = sortedNodes.first();
if (myNode.equals( smallNode)) {
// 如果是最小的節點,則表示取得鎖
System.out.println(Thread.currentThread().getName()+ myNode+"get lock");
this.nodeId.set(myNode);
return;
}
String preNode = sortedNodes.lower(myNode);
CountDownLatch latch = new CountDownLatch(1);
Stat stat = zk.exists(preNode, new LockWatcher(latch));// 同時註冊監聽。
// 判斷比自己小一個數的節點是否存在,如果不存在則無需等待鎖,同時註冊監聽
if (stat != null) {
System.out.println(Thread.currentThread().getName()+myNode+
" waiting for " + root + "/" + preNode + " released lock");
latch.await();// 等待,這裏應該一直等待其他線程釋放鎖
nodeId.set(myNode);
latch = null;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void unlock() {
try {
System.out.println(Thread.currentThread().getName()+ "unlock ");
if (null != nodeId) {
zk.delete(nodeId.get(), -1);
}
nodeId.remove();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public Condition newCondition() {
return null;
}
}
- 在隊列節點下創建臨時順序節點 例如/queue_info/192.168.1.1-0000001
- 調用getChildren()接口來獲取/queue_info節點下所有子節點,獲取隊列中所有元素
- 比較自己節點是否是序號最小的節點,如果不是,則等待其他節點出隊列,在序號最小的節點註冊 watcher
- 獲取watcher通知後,重複步驟