Spring Data JPA 之 for update
阿新 • • 發佈:2018-11-02
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 "選課成功";
}
}