1. 程式人生 > >Spring Data JPA 之 for update

Spring Data JPA 之 for update

for update問題的由來是由於高併發,且使用負載均衡時使用的。在公司有一個專案的場景,場景並不複雜:學生選課。現在有三張表,1.t_pub_student(學生資訊表),2.t_pub_course(課程資訊表),3.t_pub_course_detail(學生選課詳情)。這三張表的定義分別是:

create table t_pub_student(
id int PRIMARY key auto_increment,
code VARCHAR(50) COMMENT '學生CODE',
name VARCHAR(50) COMMENT '學生名字'
)

create table t_pub_detail(
id int PRIMARY key auto_increment,
name VARCHAR(50) COMMENT '課程名稱',
teacher_name VARCHAR(50) COMMENT '教師名字',
elective_total int COMMENT '可選總數',
elective_num int COMMENT '已選數量'
)

create table t_course_detail(
id int PRIMARY key auto_increment,
student_code varchar(50) COMMENT '學生code',
course_id int COMMENT '課程ID'
)

當學生選一門課時,會在t_course_detail插一條資料,在t_course_detail的elective_num 欄位+1。

此時就會出現問題,當Thread-1進行選課,t_course_detail插入一條資料後,查新elective_num=10,但出現處理問題,執行緒暫停。Thread-2此時也進入,進行選課,在 t_course_detail插入資料後,查詢elective_num=10(因為Thead-1還沒來得及更新t_course,elective_num仍為10,這個就是問題的原因),它繼續執行,將elective_num+1操作,elective_num=11退出執行緒。又過了幾秒,Thread-1復活,進行elective_num(值為10,復活前查詢的值)+1操作,此時elective_num=11。就會出現t_course_detail有12條記錄,t_course中的elective_num值卻為11。

 

上面問題可以通過java的鎖機制處理,但進行負載均衡的話,鎖機制就無法控制了,就需要進行資料庫的行級鎖處理,即for update。下面我會將處理的邏輯程式碼分享出來,並新增上詳細註釋。

程式碼的目錄結構:

定義實體類:

/**學生*/
@Entity
@Table(name = "t_pub_student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(name = "code")
    private String code;

    @Column(name = "name")
    private String name;
    
    Getter...
    Setter...
}
/**課程*/
@Entity
@Table(name = "t_pub_course")
public class Course {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(name = "name")
    private String name;

    @Column(name = "teacher_name")
    private String teacherName;

    @Column(name = "elective_total")
    private Integer electiveTotal;

    @Column(name = "elective_num")
    private Integer electiveNum;
    
    Getter...
    Setter...
}
/**選課詳情*/
@Entity
@Table(name = "t_course_detail")
public class CourseDetail {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(name = "course_id")
    private Integer courseId;

    @Column(name = "student_code")
    private String studentCode;
    
    Getter...
    Setter...
}

定義DAO:

public interface StudentRepository extends JpaRepository<Student,Integer>,JpaSpecificationExecutor<Student> {
}
public interface CourseDetailRepository extends JpaRepository<CourseDetail,Integer>,JpaSpecificationExecutor<CourseDetail> {
}
package course.repository;

import course.entity.Course;
import org.springframework.data.jpa.repository.*;
import javax.persistence.LockModeType;

/**
 * Created by Xichuan on 2018-10-31.
 */
public interface CourseRepository extends JpaRepository<Course,Integer>,JpaSpecificationExecutor<Course> {

    /**@Lock 作用的for update作用一樣,將此行資料進行加鎖,當整個方法將事務提交後,才會解鎖*/
    @Lock(value = LockModeType.PESSIMISTIC_WRITE)
    @Query(value = "select t from Course t where t.id =?1 ")
    Course queryAllById( Integer courseId);

    /**將course表中的electiveNum進行加1操作*/
    @Modifying
    @Query("update Course t set t.electiveNum = t.electiveNum + 1 where t.id =?1")
    void addElectiveNumByCourseId(Integer courseId);
}

定義介面:

package course.controller;

import course.service.CourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created by XiChuan 2018-10-31.
 */
@RestController
public class CourseController {

    @Autowired
    CourseService courseService;

    @PostMapping("/course/choose")
    public Object chooseCourse(@RequestParam("student_code")String studentCode,
                               @RequestParam("course_id")Integer courseId){
        return  courseService.chooseCourse(studentCode,courseId);
    }
}

service程式碼:

public interface CourseService {
    Object chooseCourse(String studentCode,Integer courseId);
}
package course.service.impl;

import course.entity.Course;
import course.entity.CourseDetail;
import course.repository.CourseDetailRepository;
import course.repository.CourseRepository;
import course.repository.StudentRepository;
import course.service.CourseService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import java.util.Objects;

/**
 * Created by XiChuan 2018-10-31.
 */
@Service
public class CourseServiceImpl implements CourseService {
    private Logger logger = LoggerFactory.getLogger(CourseServiceImpl.class);

    @Autowired
    StudentRepository studentRepository;

    @Autowired
    CourseRepository courseRepository;

    @Autowired
    CourseDetailRepository courseDetailRepository;


    /**使用for update一定要加上這個事務
     * 當事務處理完後,for update才會將行級鎖解除*/
    @Transactional(isolation = Isolation.READ_COMMITTED)
    @Override
    public Object chooseCourse(String studentCode, Integer courseId) {

        /** courseRepository.queryAllById(courseId)會對所選中的那條記錄加行級鎖,其他執行緒會在此排隊,當事務提交後,才會進行解鎖*/
        Course course = courseRepository.queryAllById(courseId);

        int electiveNum = course.getElectiveNum();
        int totalNum = course.getElectiveTotal();
        logger.info("After Lock Step 1, Thread: {},courseId{}, studentId: {}, electiveNum: {}, total: {}", Thread.currentThread(),courseId,studentCode, electiveNum, totalNum);

        if (Objects.isNull(course)){
            return "課程不存在";
        }
        if (electiveNum >= totalNum) {
            return "此課程已被選完";
        }

        /**將此此學生的選課資訊儲存到選課詳情裡面*/
        CourseDetail courseDetail = new CourseDetail();
        courseDetail.setCourseId(courseId);
        courseDetail.setStudentCode(studentCode);
        courseDetailRepository.save(courseDetail);

        /**將course表中的electiveNum進行加1操作
         * 使用sql進行累加更加安全,因為使用方法開始查詢的course中的electiveNum,並不一定是資料庫儲存的值*/
        courseRepository.addElectiveNumByCourseId(courseId);
        return "選課成功";
    }
}