SpringBoot通過RedisTemplate執行Lua腳本


   Redis使用的是內存,內存的速度比磁盤速度肯定要快很多.。使用 Redis實現搶紅包,需要知道的是Redis的功能不如數據庫強大,事務也不是很完整.因此要保證數據的正確性,可以通過嚴格的驗證得以保證。而 Redis的 Lua 語言是原子性的,且功能更為強大,所以優先選擇使用Lua語言來實現搶紅包。

  在 Redis 當中存儲,始終都不是長久之計 , 因為 Redis並非一個長久儲存數據的地方,更多的時候只是為了提供更為快速的緩存,所以當紅包金額為 0 或者紅包超時的時候(超時操作可以使用定時機制實,這里暫不討論), 會將紅包數據保存到數據庫中, 這樣才能夠保證數據的安全性和嚴格性。

  注解方式配置 Redis

@Configuration
public class RedisHepler { @Bean(name = "redisTemplate") public RedisTemplate initRedisTemplate(LettuceConnectionFactory connectionFactory) { RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>(); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new StringRedisSerializer()); redisTemplate.setConnectionFactory(connectionFactory); return redisTemplate; } @Bean public DefaultRedisScript<String> defaultRedisScript() { DefaultRedisScript<String> defaultRedisScript = new DefaultRedisScript<>(); defaultRedisScript.setResultType(String.class); defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/demo.lua"))); return defaultRedisScript; } }

  lua腳本和異步持久化功能的開發

  Redis 中的 Lua 語言是一種原子性的操作,可以保證數據的一致性 。

local listKey = 'red_packet_list_'..KEYS[1]
local redPacket = 'red_packet_'..KEYS[1]
local stock=tonumber(redis.call('hget',redPacket,'stock'))
if
    stock<=0 then return 0
end
   stock =stock-1
   redis.call('hset',redPacket,'stock',tostring(stock))
   redis.call('rpush',listKey,ARGV[1])
if
    stock==0 then return 2
end
   return 1

流程:

  判斷是否存在可搶的庫存,如果己經沒有可搶奪的紅包,則返回為 0,結束流程
  有可搶奪的紅包,對於紅包的庫存減1 ,然后重新設置庫存
  將搶紅包數據保存到 Redis 的鏈表當中,鏈表的 key 為 red_packet_list_ {id}
  如果當前庫存為 0 ,那么返回 2,這說明可以觸發數據庫對 Redis 鏈表數據的保存,鏈表的 key 為 red_packet_ list_ {id},它將保存搶紅包的用戶名和搶的時間
  如果當前庫存不為 0 ,那么將返回 1,這說明搶紅包信息保存成功。

  當返回為 2 的時候,說明紅包己經沒有庫存,會觸發數據庫對鏈表數據的保存, 這是一個大數據量的保存。為了不影響最后一次搶紅包的響應,在實際的操作中往往會考慮使用 JMS 消息發送到別的服務器進行操作

接口

package com.smart.service;

public interface RedisRedPacketService {
    /**
     * 保存redis搶紅包列表
     * @param redPacketId
     * @param unitAmount
     */
    void saveUserRedPacketByRedis(Long redPacketId, Double unitAmount);
}

實現類

package com.smart.service.impl;

import com.smart.model.UserRedPacket;
import com.smart.service.RedisRedPacketService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Service
public class RedisRedPacketServiceImpl implements RedisRedPacketService {

    private static Logger logger = LoggerFactory.getLogger(RedisRedPacketServiceImpl.class);
    private static final String PREFIX = "red_packet_list_";
    /**
     * 每次取出1000條數據
     */
    private static final int TIME_SIZE = 1000;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private DataSource dataSource;

    @Async
    public void saveUserRedPacketByRedis(Long redPacketId, Double unitAmount) {
        logger.info("----------開始保存數據----------------");
        long start = System.currentTimeMillis();
        BoundListOperations ops = redisTemplate.boundListOps(PREFIX + redPacketId);
        Long SIZE = ops.size();
        Long times = SIZE % TIME_SIZE == 0 ? SIZE/TIME_SIZE : (SIZE/TIME_SIZE+1);

        int count=0;
        List<UserRedPacket> userRedPackets=new ArrayList<>(TIME_SIZE);
        for(int i=0;i<times;i++){
            List userIdList=null;
            if(i==0){
                userIdList = ops.range(i * TIME_SIZE, (i + 1) * TIME_SIZE);
            }else{
                userIdList=ops.range(i*TIME_SIZE+1,(i+1)*TIME_SIZE);
            }
            userRedPackets.clear();
            for(int j=0;j<userIdList.size();j++){
                String  values= userIdList.get(j).toString();
                String[] arr = values.split("-");
                String userIdStr=arr[0];
                String timeStr=arr[1];
                long userId = Long.parseLong(userIdStr);
                long time = Long.parseLong(timeStr);

                UserRedPacket userRedPacket = new UserRedPacket();
                userRedPacket.setRedPacketId(redPacketId);
                userRedPacket.setUserId(userId);
                userRedPacket.setGrabTime(new Date(time));
                userRedPacket.setAmount(unitAmount);
                userRedPacket.setNote("搶紅包 "+redPacketId);
                userRedPackets.add(userRedPacket);
            }
            count+=executeBatch(userRedPackets);
        }
        redisTemplate.delete(PREFIX+redPacketId);
        long end=System.currentTimeMillis();
        System.out.println("保存數據結束,耗時" + (end - start) + "毫秒,共" + count + "條記錄被保存");
    }

    private int executeBatch(List<UserRedPacket> userRedPackets) {
        Connection conn=null;
        Statement statement=null;
        int[] count=null;
        try{
            conn = dataSource.getConnection();
            conn.setAutoCommit(false);
            statement = conn.createStatement();
            for(UserRedPacket userRedPacket :userRedPackets){
                String sql1 = "update T_RED_PACKET set stock = stock-1 where id=" + userRedPacket.getRedPacketId();
                DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                String sql2 = "insert into T_USER_RED_PACKET(red_packet_id, user_id, " + "amount, grab_time, note)"
                        + " values (" + userRedPacket.getRedPacketId() + ", " + userRedPacket.getUserId() + ", "
                        + userRedPacket.getAmount() + "," + "'" + df.format(userRedPacket.getGrabTime()) + "'," + "'"
                        + userRedPacket.getNote() + "')";
                statement.addBatch(sql1);
                statement.addBatch(sql2);
            }
            count= statement.executeBatch();
            conn.commit();
        } catch (SQLException e) {
            throw new RuntimeException("搶紅包批量執行程序錯誤");
        }finally {
            try {
                if(conn!=null&&!conn.isClosed()){
                    conn.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return count.length/2;
    }

}

  注解@Async 表示讓 Spring 自動創建另外一條線程去運行它,這樣它便不在搶最后一個紅包的線程之內。因為這個方法是一個較長時間的方法,在同一個線程內,那么對於最后搶紅包的用戶需要等待的時間太長,用戶體驗不好

  這里是每次取出 1 000 個搶紅包的信息,之所以這樣做是為了避免取出 的數據過大 , 導致JVM 消耗過多的內存影響系統性能。

  對於大批量的數據操作,在實際操作中要注意的,最后還會刪除 Redis保存的鏈表信息,這樣就幫助 Redis 釋放內存,對於數據庫的保存 ,這里采用了 JDBC的批量處理,每 1000 條批量保存一次,使用批量有助於性能的提高。

  Service層添加Redis搶紅包的邏輯

  UserRedPacketService接口新增接口方法grapRedPacketByRedis

/**
     * 通過Redis實現搶紅包
     * 
     * @param redPacketId    --紅包編號
     * @param userId         -- 用戶編號
     * @return 0-沒有庫存,失敗 1--成功,且不是最后一個紅包 2--成功,且是最后一個紅包
     */
    public Long grapRedPacketByRedis(Long redPacketId, Long userId);

  實現類

package com.smart.service.impl;

import com.smart.service.RedisRedPacketService;
import com.smart.service.UserRedPacketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.io.Serializable;

@Service
public class UserRedPacketServiceImpl implements UserRedPacketService {

    @Autowired
    RedisTemplate<String,Serializable> redisTemplate;
    @Autowired
    RedisRedPacketService redisRedPacketService;
    @Autowired
    DefaultRedisScript<String> redisScript;

    @Override
    public Long grapRedPacketByRedis(Long redPacketId, Long userId) {
        String args=userId+"-"+System.currentTimeMillis();
        String key=String.valueOf(redPacketId);

        Object res = redisTemplate.execute((RedisConnection connection) -> connection.eval(
                redisScript.getScriptAsString().getBytes(),
                ReturnType.INTEGER,
                1,
                key.getBytes(),
                args.getBytes()));
        Long result= (Long) res;
        if(result==2){
            String unitAmountStr = (String) redisTemplate.opsForHash().get("red_packet_" + redPacketId,"unit_amount");
            Double unitAmount = Double.valueOf(unitAmountStr);
            redisRedPacketService.saveUserRedPacketByRedis(redPacketId,unitAmount);
        }
        return result;
    }
}

  Controller層新增路由方法

package com.smart.controller;

import com.smart.service.UserRedPacketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

@Controller
@RequestMapping("/userRedPacket")
public class UserRedPacketController {

    @Autowired
    private UserRedPacketService userRedPacketService;

    @RequestMapping("/grapRedPacketByRedis")
    @ResponseBody
    public Map<String,Object> grapRedPacketByRedis(Long redPacketId,Long userId){
        Map<String, Object> resultMap = new HashMap<String, Object>();
        Long result = userRedPacketService.grapRedPacketByRedis(redPacketId, userId);
        boolean flag=result>0;
        resultMap.put("result",flag);
        resultMap.put("message",flag ? "秒殺成功":"秒殺異常");
        return resultMap;
    }
}

  先在 Redis 上添加紅包信息

127.0.0.1:6379> HMSET red_packet_1 stock 20000 unit_amount 10
OK

  grapRedPacket.html

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>參數</title>
    <!-- 加載Query文件-->
    <script type="text/javascript"
            src="https://code.jquery.com/jquery-3.2.0.js">
    </script>
    <script type="text/javascript">
        $(document).ready(function () {
            //模擬30000個異步請求,進行並發
            var max = 30000;
            for (var i = 1; i <= 1; i++) {
                $.post({
                    //請求搶id為1的紅包
                    //根據自己請求修改對應的url和大紅包編號
                    url: "./userRedPacket/grapRedPacketByRedis?redPacketId=1&userId="+i,
                    //成功后的方法
                    success: function (result) {
                        document.write(i);
                        if(i%1000==0){
                            document.write("\n");
                        }
                    }
                });
            }
        });
    </script>
</head>
<body>
</body>
</html>

  訪問:http://localhost:8080/grapRedPacket.html

 

  

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM