1. 程式人生 > 其它 >Redis(三)jedis與鎖

Redis(三)jedis與鎖

1 Jedis

引入依賴
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>4.2.3</version>
        </dependency>
測試連通
    public static void main(String[] args) {
        String host = "192.168.60.100";
        int port = 6379;

        Jedis jedis = new Jedis(host, port);
        String ping = jedis.ping();
        System.out.println(ping);
    }

輸出“PONG”表示連線Redis成功

如果顯示連線超時,則檢查以下幾點:

① 檢查配置檔案bind以及protectedmodel

② 檢查防火牆是否關閉

API 基本是前面的命令 這裡略過了
案例:模擬簡訊驗證碼
    public static void main(String[] args) {
        verifyCodeSend("1568887221");
        System.out.println(verifyCode("1568887221", "94987"));
    }

    public static void verifyCodeSend(String phoneNum) {
        String host = "192.168.60.100";
        int port = 6379;
        Jedis jedis = new Jedis(host, port);

        String countKey = "VERIFY_CODE_COUNT_" + phoneNum;
        String codeKey = "VERIFY_CODE_" + phoneNum;

        // 每個手機每天只能傳送三次
        String count = jedis.get(countKey);
        if(count == null) {
            jedis.setex(countKey, 24 * 60 * 60, "1");
        } else if(Integer.parseInt(count) <= 2) {
            jedis.incr(countKey);
        } else {
            System.out.println("傳送三次大於三了");
            jedis.close();
            return ;
        }
        // 驗證碼傳送
        String code = generateCode();

        jedis.setex(codeKey, 5 * 60, code);

        System.out.println("傳送成功" + code);

        jedis.close();
    }


    public static boolean verifyCode(String phoneNum, String code) {
        String host = "192.168.60.100";
        int port = 6379;
        Jedis jedis = new Jedis(host, port);

        String codeKey = "VERIFY_CODE_" + phoneNum;

        String codeRedis = jedis.get(codeKey);

        if(codeRedis == null) {
            System.out.println("手機號錯誤");
            jedis.close();
            return false;
        } else {
            boolean result = codeRedis.equals(code);
            jedis.close();
            return result;
        }

    }

    public static String generateCode() {
        Random random = new Random();
        StringBuilder str = new StringBuilder();
        for(int i = 0; i < 6; i++) {
            str.append(random.nextInt(10));
        }
        return str.toString();
    }

感覺這裡老師講的邏輯好像不大對,上面是進行修改後的

2 SpringBoot整合Redis

引入依賴
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
配置redis
spring:
  redis:
    host: 192.168.60.100
    port: 6379
#    預設資料庫連線索引
    database: 0
    timeout: 1800000
#    連線池最大連線數(預設為8,負數表示無限制)
    lettuce:
      pool:
        max-active: 20
    jedis:
      pool:
#        最大阻塞等待時間(預設為-1)
        max-wait: -1
#        最大空閒連線(預設為8)
        max-idle: 5
#         最小空閒連線(預設為-1)
        min-idle: 0

3 Redis事務和鎖操作

3.1 簡介

Redis事務是一個單獨的隔離操作:事務中所有的命令都會被序列化按照順序執行。事務在執行過程中,不會被客戶端傳送來的其他命令打斷。

Redis事務的主要作用就是串聯多個命令防止別的命令插隊。

3.2 基本命令
Multi 開啟事務
Exec 執行事務
Discard 放棄組隊

從輸入Multi開始,輸入的命令都會依次進入命令佇列,但不會執行,直到輸入Exec後Redis將會依次執行命令佇列中的命令。並在組隊的過程中可以使用dicard來放棄組隊。

3.3 兩個例項
組隊期間的錯誤(即編譯的語法錯誤)
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 value
QUEUED
127.0.0.1:6379(TX)> set key2
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> 

可以看到結果是直接無法排隊

執行期間的錯誤
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 v1
QUEUED
127.0.0.1:6379(TX)> incr key1
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range

可以看到組隊成功但執行失敗,但是第一條語句還是能夠執行,也就是Redis

3.4 悲觀鎖與樂觀鎖解決事務衝突問題

悲觀鎖:顧名思義就是很悲觀,每次去獲取資料的時候都認為別的事務操作會進行修改,因此需要加鎖保證別的事務拿不到資料。傳統的關係型資料庫裡面用到了很多這種鎖機制,比如行鎖、表鎖、讀鎖、寫鎖等,都用到了這種鎖的機制。

樂觀鎖:樂觀鎖則是認為每次獲取資料的時候都沒有其他事務修改資料,因此不會上鎖,只是對資料新增一個版本欄位,只有當自己修改資料的時候才去檢查當前資料的版本欄位和之前自己的版本欄位,如果不一致則取消更新。

樂觀鎖適用於多讀的應用型別,這樣可以提高吞吐量,Redis就是利用這種check-and-set實現事務的

搶票就是樂觀鎖的一個典型應用場景,即很多人搶票只能有一個人成功,如果使用悲觀鎖的話雖然也能夠實現但是 單位時間 微觀上 只能有一個人在搶票,系統的吞吐量太小

樂觀鎖基本命令 watch 樂觀鎖監視資料
127.0.0.1:6379> get balance
"100"
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby balance 10
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 110
127.0.0.1:6379> get balance
"100"
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby balance 10
QUEUED
127.0.0.1:6379(TX)> exec
(nil)

可以看到第二個客戶端的事務監測到資料版本和之前的不同終止了事務

樂觀鎖基本命令 unwatch 停止對所有資料的監視
3.5 Redis事務的三大特性
  • 單獨的隔離操作

    • 事務中的所有命令都會序列化 按照順序執行,不會被其他客戶端發來的命令所打斷
  • 沒有隔離級別的概念

    • 佇列中的命令沒有提交之前都不會被實際執行
  • 不保證原子性

    • 事務中如果有一條命令執行失敗,其他命令依然會被執行,沒有回滾

4 事務和鎖機制-秒殺案例

案例分析

在redis中使用欄位sk:product:qt儲存庫存,使用set儲存購買成功的使用者id

原始程式碼
    //秒殺過程
    public static boolean doSecKill(String uid,String prodid) throws IOException {
        //1 uid和prodid非空判斷
        if(uid == null || prodid == null) {
            return false;
        }

        //2 連線redis
        //Jedis jedis = new Jedis("192.168.44.168",6379);
        //通過連線池得到jedis物件
        Jedis jedis = new Jedis("192.168.60.100", 6379);

        //3 拼接key
        // 3.1 庫存key
        String kcKey = "sk:" + prodid + ":qt";

        // 3.2 秒殺成功使用者key
        String userKey = "sk:" + uid + ":user";


        //4 獲取庫存,如果庫存null,秒殺還沒有開始
        String kc = jedis.get(kcKey);
        if(kc == null) {
            System.out.println(uid + "您好,秒殺還沒有開始");
            jedis.close();
            return false;
        }

        // 5 判斷使用者是否重複秒殺操作
        if(jedis.sismember(userKey, uid)) {
            System.out.println(uid + "您好,不能重複購買");
            jedis.close();
            return false;
        }

        //6 判斷如果商品數量,庫存數量小於1,秒殺結束
        if(Integer.parseInt(jedis.get(kcKey)) <= 0) {
            System.out.println(uid + "您好,秒殺活動已經結束");
            jedis.close();
            return false;
        }


        //7 秒殺過程
        //7.1 庫存-1
        jedis.decr(kcKey);
        System.out.println(uid+",購買成功");
        //7.2 把秒殺成功使用者新增清單裡面
        jedis.sadd(userKey, uid);
        jedis.close();

        return true;
    }

控制檯的輸出:

34074,購買成功
39830,購買成功
19620,購買成功
9727,購買成功
31847您好,秒殺活動已經結束

此時的redis:

127.0.0.1:6379> keys *
 1) "sk:0101:qt"
 2) "sk:9727:user"
 3) "sk:2618:user"
 4) "sk:34074:user"
 5) "sk:39446:user"
 6) "sk:19620:user"
 7) "sk:19756:user"
 8) "sk:39830:user"
 9) "sk:14215:user"
10) "sk:39589:user"
11) "sk:33691:user"
127.0.0.1:6379> get sk:0101:qt
"0"
使用ab工具進行高併發檢測
工具安裝
yum install httpd-tools
基本命令

ab -n -c -p -T

-n 表示請求的數量

-c 表示請求中併發的數量

-p 表示請求為post請求的時候的內容

-T 表示請求的型別

             'application/x-www-form-urlencoded'
             Default is 'text/plain'
測試
ab -n 1000 -c 100 -p ./postfile -T application/x-www-form-urlencoded http://192.168.1.108//Seckill/doseckill
Percentage of the requests served within a certain time (ms)
  50%     98
  66%    103
  75%    107
  80%    109
  90%    115
  95%    118
  98%    121
  99%    123
 100%    128 (longest request)

此時java中的控制檯

34074,購買成功
39830,購買成功
19620,購買成功
9727,購買成功
31847您好,秒殺活動已經結束
05-Nov-2022 18:15:06.190 資訊 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory 把web 應用程式部署到目錄 [D:\software\apache-tomcat-8.5.78\webapps\manager]
05-Nov-2022 18:15:06.222 資訊 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Web應用程式目錄[D:\software\apache-tomcat-8.5.78\webapps\manager]的部署已在[33]毫秒內完成
49635,購買成功
20104,購買成功
20281,購買成功
38295,購買成功
36060,購買成功
20169,購買成功
16599,購買成功
1757,購買成功
27781,購買成功
37401,購買成功
24778,購買成功
16002您好,秒殺活動已經結束
47051,購買成功
47757,購買成功
47240您好,秒殺活動已經結束
33393您好,秒殺活動已經結束
6163您好,秒殺活動已經結束
4882,購買成功
17448您好,秒殺活動已經結束
19355您好,秒殺活動已經結束
6336您好,秒殺活動已經結束
47492,購買成功
43854您好,秒殺活動已經結束
7106您好,秒殺活動已經結束
44598,購買成功
7747,購買成功
9785,購買成功
25385,購買成功
1499,購買成功
26152,購買成功
31252,購買成功
4589,購買成功
30801,購買成功
13182,購買成功
6426,購買成功
46654,購買成功
45300,購買成功
43455,購買成功
24394您好,秒殺活動已經結束
6417您好,秒殺活動已經結束
14826您好,秒殺活動已經結束
37355,購買成功
22473您好,秒殺活動已經結束
45251,購買成功
30914,購買成功
13849,購買成功
39553您好,秒殺活動已經結束
9444,購買成功
954您好,秒殺活動已經結束
133您好,秒殺活動已經結束

redis中:

127.0.0.1:6379> get sk:0101:qt
"-27"
超賣和超時問題解決

超時問題:使用資料庫連線池

public class JedisPoolUtil {
    private static volatile JedisPool jedisPool = null;

    private JedisPoolUtil() {
    }

    public static JedisPool getJedisPoolInstance() {
        if (null == jedisPool) {
            synchronized (JedisPoolUtil.class) {
                if (null == jedisPool) {
                    JedisPoolConfig poolConfig = new JedisPoolConfig();
                    poolConfig.setMaxTotal(200);
                    poolConfig.setMaxIdle(32);
                    poolConfig.setMaxWaitMillis(100*1000);
                    poolConfig.setBlockWhenExhausted(true);
                    poolConfig.setTestOnBorrow(true);  // ping  PONG

                    jedisPool = new JedisPool(poolConfig, "192.168.44.168", 6379, 60000 );
                }
            }
        }
        return jedisPool;
    }

    public static void release(JedisPool jedisPool, Jedis jedis) {
        if (null != jedis) {
            jedisPool.returnResource(jedis);
        }
    }

}

超賣問題 : 使用樂觀鎖檢測資料並使用事務操作

    //秒殺過程
    public static boolean doSecKill(String uid,String prodid) throws IOException {
        //1 uid和prodid非空判斷
        if(uid == null || prodid == null) {
            return false;
        }

        //2 連線redis
        //Jedis jedis = new Jedis("192.168.44.168",6379);
        //通過連線池得到jedis物件
        JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
        Jedis jedis = jedisPoolInstance.getResource();

        //3 拼接key
        // 3.1 庫存key
        String kcKey = "sk:"+prodid+":qt";
        // 3.2 秒殺成功使用者key
        String userKey = "sk:"+prodid+":user";

        //監視庫存
        jedis.watch(kcKey);

        //4 獲取庫存,如果庫存null,秒殺還沒有開始
        String kc = jedis.get(kcKey);
        if(kc == null) {
            System.out.println("秒殺還沒有開始,請等待");
            jedis.close();
            return false;
        }

        // 5 判斷使用者是否重複秒殺操作
        if(jedis.sismember(userKey, uid)) {
            System.out.println("已經秒殺成功了,不能重複秒殺");
            jedis.close();
            return false;
        }

        //6 判斷如果商品數量,庫存數量小於1,秒殺結束
        if(Integer.parseInt(kc)<=0) {
            System.out.println("秒殺已經結束了");
            jedis.close();
            return false;
        }

        //7 秒殺過程
        //使用事務
        Transaction multi = jedis.multi();

        //組隊操作
        multi.decr(kcKey);
        multi.sadd(userKey,uid);

        //執行
        List<Object> results = multi.exec();

        if(results == null || results.size()==0) {
            System.out.println("秒殺失敗了....");
            jedis.close();
            return false;
        }

        //7.1 庫存-1
        //jedis.decr(kcKey);
        //7.2 把秒殺成功使用者新增清單裡面
        //jedis.sadd(userKey,uid);

        System.out.println("秒殺成功了..");
        jedis.close();
        return true;
    }
lua解決樂觀鎖造成的庫存遺留

問題分析:當併發度特別高的時候,會出現沒有結束的情況

ab -n 1000 -c 400 -p ./postfile -T application/x-www-form-urlencoded http://192.168.1.108//Seckill/doseckill

然後redis中的庫存顯示為:

127.0.0.1:6379> get sk:0101:qt
"13"
public class SecKill_redisByScript {

    private static final  org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;

    public static void main(String[] args) {
        JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();

        Jedis jedis=jedispool.getResource();
        System.out.println(jedis.ping());

        Set<HostAndPort> set=new HashSet<HostAndPort>();

    //    doSecKill("201","sk:0101");
    }

    static String secKillScript ="local userid=KEYS[1];\r\n" + 
            "local prodid=KEYS[2];\r\n" + 
            "local qtkey='sk:'..prodid..\":qt\";\r\n" + 
            "local usersKey='sk:'..prodid..\":usr\";\r\n" + 
            "local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" + 
            "if tonumber(userExists)==1 then \r\n" + 
            "   return 2;\r\n" + 
            "end\r\n" + 
            "local num= redis.call(\"get\" ,qtkey);\r\n" + 
            "if tonumber(num)<=0 then \r\n" + 
            "   return 0;\r\n" + 
            "else \r\n" + 
            "   redis.call(\"decr\",qtkey);\r\n" + 
            "   redis.call(\"sadd\",usersKey,userid);\r\n" + 
            "end\r\n" + 
            "return 1" ;

    static String secKillScript2 = 
            "local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +
            " return 1";

    public static boolean doSecKill(String uid,String prodid) throws IOException {

        JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
        Jedis jedis=jedispool.getResource();

         //String sha1=  .secKillScript;
        String sha1=  jedis.scriptLoad(secKillScript);
        Object result= jedis.evalsha(sha1, 2, uid,prodid);

          String reString=String.valueOf(result);
        if ("0".equals( reString )  ) {
            System.err.println("已搶空!!");
        }else if("1".equals( reString )  )  {
            System.out.println("搶購成功!!!!");
        }else if("2".equals( reString )  )  {
            System.err.println("該使用者已搶過!!");
        }else{
            System.err.println("搶購異常!!");
        }
        jedis.close();
        return true;
    }
}

此時的redis:

[root@hadoop100 hikaru]# docker exec -it redis redis-cli
127.0.0.1:6379> keys *
1) "sk:0101:qt"
2) "sk:0101:usr"
127.0.0.1:6379> get sk:0101:qt
"0"

這裡也沒有講太清楚。。lua的作用就是把兩個操作(減少庫存並新增使用者)變成了原子性操作,實際上就是變成了使用了悲觀鎖?只不過因為redis沒有悲觀鎖嗎

查了一下網上說:減庫存邏輯其實就是先是用lua指令碼減redis庫存,如果成功再去減資料庫中的真實庫存,如果減redis庫存失敗,庫存不足,就不會再走後面減真實庫存的邏輯了。

5 Redis持久化

5.1 RDB
簡介

在指定的時間間隔內,將記憶體中的資料集快照寫入磁碟